Files
youle_app_ios_v2/ylgamehall/Source/Location/LocationService.swift
T
joywayerandClaude Opus 5 3f8b382ad3 加桥/定位边界诊断日志,排查子游戏收不到 getlocationinfo
现象:大厅 H5 能正确拿到定位,子游戏拿不到。已静态排除「子游戏未注册
handler」与「回调错发给大厅」两个猜测(BridgeBus 是 per-WebView 实例、
handlers 是实例状态,反向 call 捕获各自的 bridge,物理上不可能串台;
下载真实子游戏 zip 比对后确认 H5 侧大厅/子游戏逻辑完全对称)。

为定位真正的失败层,在各组件边界加日志(纯诊断,不改任何行为):
- BridgeBus 加 label(lobby / subGame)区分来源,记录 ← H5 调用、
  → H5 反向 call、native handler 未注册、evaluateJavaScript 失败
- sendToJS 改为返回 'ok'/'no-bridge',H5 侧 bridge 未就绪导致的静默丢包
  现在会打错误日志(原实现 `if (window.X)` 直接丢弃,完全不可见)
- LocationService 记录 requestOnce 序号 / shared manager id /
  requestLocation 的 BOOL 返回值 / completionBlock 是否回来 / stop() 调用
  —— 高德文档明确 requestLocation 返回 NO 时 completionBlock 永不调用,
  当前代码忽略该返回值会让 continuation 永挂,H5 连 errorCode 12 都收不到
- H5ErrorRelay 补 console.warn 中继,让 WVJB「H5 侧无对应 handler」的
  warn 能出现在 Xcode console

契约影响:无。仅新增日志与可选 label 参数,桥接接口名 / 字段名 / 数据结构
均未变动(docs/H5-Native-Contract.md §3.1[20]/ §3.2[6]不变)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMPCLfsuxvwgMotzsb67QH
2026-08-07 01:54:18 +08:00

125 lines
5.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// LocationService.swift
// ylgamehall
//
// 高德 AMapLocationManager 异步包装:一次性 / 持续两种模式。
// 契约 §3.2 6 getlocationinfo 9 字段;msext gameController.m:2491-2557 等价。
//
// ⚠️ canImport(AMapLocationKit) 守卫:SDK 未链接时 LocationService 永远抛
// LocationError.sdkNotLinkedhandler 兜底退化为 stub 行为。
//
import Foundation
import CoreLocation
import os.log
/// 定位诊断日志(文件级 letLogger 是 Sendable,便于在 AMap 回调闭包内直接用)。
private let locLog = Logger(subsystem: "ylgamehall", category: "Location")
#if canImport(AMapLocationKit)
import AMapLocationKit
#endif
#if canImport(AMapFoundationKit)
import AMapFoundationKit
#endif
/// 9 字段定位结果。字段名严格 1:1 H5 端契约(contract §3.2 表 [6]):
/// - latitude / longitude **是 string** (`String(format: "%f", ...)`)
/// - province **小写 p**(与 sharelogin.Province 大写不同)
public struct LocationPayload: Sendable {
public let address: String
public let city: String
public let cityCode: String
public let country: String
public let district: String
public let latitude: String
public let longitude: String
public let province: String
public let street: String
}
public enum LocationError: Error, Sendable {
case sdkNotLinked
case authorizationDenied
case timeout
case underlying(any Error)
}
@MainActor
public final class LocationService {
public static let shared = LocationService()
public init() {}
#if canImport(AMapLocationKit)
private let manager: AMapLocationManager = {
let m = AMapLocationManager()
m.desiredAccuracy = kCLLocationAccuracyHundredMeters
m.locationTimeout = 6
m.reGeocodeTimeout = 3
m.locatingWithReGeocode = true
return m
}()
#endif
/// 诊断用:requestOnce 调用序号,日志里可对上「谁发起、谁回来」。
private var requestSeq: UInt64 = 0
/// 一次性定位 + 逆地理(msext locAction + completionBlock 等价)
public func requestOnce(caller: String = "?") async throws -> LocationPayload {
#if canImport(AMapLocationKit)
requestSeq += 1
let seq = requestSeq
locLog.debug("[\(caller, privacy: .public)] requestOnce #\(seq, privacy: .public) 发起(shared manager id=\(String(describing: ObjectIdentifier(self.manager)), privacy: .public)")
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<LocationPayload, Error>) in
let accepted = manager.requestLocation(withReGeocode: true) { loc, regeo, err in
locLog.debug("[\(caller, privacy: .public)] requestOnce #\(seq, privacy: .public) completionBlock 回来:loc=\(loc != nil, privacy: .public) regeo=\(regeo != nil, privacy: .public) err=\(err?.localizedDescription ?? "nil", privacy: .public)")
if let err = err as NSError? {
// msext 行为:权限相关错误归一到 12 errorCodehandler 转字段)
if err.code == AMapLocationErrorCode.locateFailed.rawValue {
cont.resume(throwing: LocationError.authorizationDenied)
} else {
cont.resume(throwing: LocationError.underlying(err))
}
return
}
guard let loc, let regeo else {
cont.resume(throwing: LocationError.timeout)
return
}
cont.resume(returning: LocationPayload(
address: regeo.formattedAddress ?? "",
city: regeo.city ?? "",
cityCode: regeo.citycode ?? "",
country: regeo.country ?? "",
district: regeo.district ?? "",
latitude: String(format: "%f", loc.coordinate.latitude),
longitude: String(format: "%f", loc.coordinate.longitude),
province: regeo.province ?? "",
street: regeo.street ?? ""
))
}
// ⚠️ AMapLocationManager.h:「是否成功添加单次定位Request」。返回 NO 时
// completionBlock 永远不会被调用 → continuation 永挂 → H5 收不到任何
// getlocationinfo(连 errorCode 12 都没有)。诊断阶段先只记录。
locLog.debug("[\(caller, privacy: .public)] requestOnce #\(seq, privacy: .public) requestLocation 返回 accepted=\(accepted, privacy: .public)")
}
#else
locLog.error("[\(caller, privacy: .public)] requestOnce: AMapLocationKit 未链接,抛 sdkNotLinked")
throw LocationError.sdkNotLinked
#endif
}
/// 停止:msext gameController.m:2473-2478 cleanUpAction 等价
public func stop(caller: String = "?") {
#if canImport(AMapLocationKit)
// ⚠️ AMapLocationManager.hstopUpdatingLocation「会 cancel 掉所有的单次定位请求」。
// 本类是 shared 单例、manager 唯一,因此这里会连带取消**其它容器**在飞的单次定位。
locLog.debug("[\(caller, privacy: .public)] stop()stopUpdatingLocation + delegate=nil(会取消所有在飞的单次定位)")
manager.stopUpdatingLocation()
manager.delegate = nil
#endif
}
}