现象:大厅 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
125 lines
5.4 KiB
Swift
125 lines
5.4 KiB
Swift
//
|
||
// LocationService.swift
|
||
// ylgamehall
|
||
//
|
||
// 高德 AMapLocationManager 异步包装:一次性 / 持续两种模式。
|
||
// 契约 §3.2 [6] getlocationinfo 9 字段;msext gameController.m:2491-2557 等价。
|
||
//
|
||
// ⚠️ canImport(AMapLocationKit) 守卫:SDK 未链接时 LocationService 永远抛
|
||
// LocationError.sdkNotLinked,handler 兜底退化为 stub 行为。
|
||
//
|
||
|
||
import Foundation
|
||
import CoreLocation
|
||
import os.log
|
||
|
||
/// 定位诊断日志(文件级 let,Logger 是 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 errorCode(handler 转字段)
|
||
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.h:stopUpdatingLocation「会 cancel 掉所有的单次定位请求」。
|
||
// 本类是 shared 单例、manager 唯一,因此这里会连带取消**其它容器**在飞的单次定位。
|
||
locLog.debug("[\(caller, privacy: .public)] stop():stopUpdatingLocation + delegate=nil(会取消所有在飞的单次定位)")
|
||
manager.stopUpdatingLocation()
|
||
manager.delegate = nil
|
||
#endif
|
||
}
|
||
}
|