修复定位的回调问题

This commit is contained in:
joywayer
2026-08-07 12:18:44 +08:00
parent 3f8b382ad3
commit 1203957924
12 changed files with 637 additions and 180 deletions
+134 -16
View File
@@ -14,6 +14,70 @@ import os.log
/// MainActor
private let bridgeLog = Logger(subsystem: "ylgamehall", category: "Bridge")
/// getlocationinfo
///
/// `Logger.debug` devicectl console / Xcode
/// console `.error` `print` stderr
/// 便 CoreLocation os_log
/// `Documents/diag.log` Xcode print Xcode console
///
/// xcrun devicectl device copy from --domain-type appDataContainer \
/// --domain-identifier com.skyapp.ylgamehall --source Documents/diag.log ...
///
nonisolated func diagLog(_ message: String) {
let line = "[DIAG \(diagTimestamp())] \(message)"
print(line)
diagFileSink.append(line)
}
private nonisolated let diagFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "HH:mm:ss.SSS"
return f
}()
private nonisolated func diagTimestamp() -> String {
diagFormatter.string(from: Date())
}
/// `Documents/diag.log` session
/// 便 diagLog
// SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor nonisolated
// @Sendable / evaluateJavaScript
private nonisolated final class DiagFileSink: @unchecked Sendable {
private let queue = DispatchQueue(label: "ylgamehall.diag.log")
private let url: URL
private var wroteHeader = false
init() {
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
url = dir.appendingPathComponent("diag.log")
}
func append(_ line: String) {
queue.async { [self] in
var text = line + "\n"
if !wroteHeader {
wroteHeader = true
let stamp = ISO8601DateFormatter().string(from: Date())
text = "\n===== session \(stamp) pid=\(ProcessInfo.processInfo.processIdentifier) =====\n" + text
}
guard let data = text.data(using: .utf8) else { return }
let fm = FileManager.default
if !fm.fileExists(atPath: url.path) {
fm.createFile(atPath: url.path, contents: nil)
}
guard let handle = try? FileHandle(forWritingTo: url) else { return }
handle.seekToEndOfFile()
handle.write(data)
try? handle.close()
}
}
}
private nonisolated let diagFileSink = DiagFileSink()
/// H5 Native 线
///
/// **** WKWebView @MainActor 线访
@@ -38,6 +102,18 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
private var pendingCallbacks: [String: BridgeCallback] = [:]
private var nativeCallbackCounter: UInt64 = 0
/// msext `WebViewJavascriptBridgeBase.startupMessageQueue`
/// `_queueMessage` bridge loaded `injectJavascriptFile` flush
///
/// `window.WebViewJavascriptBridge` ****
/// `sendToJS 'getnetwork' H5no-bridge`
/// msext H5 WebView H5
/// `startlocation` `getlocationinfo`
private var startupMessageQueue: [[String: Any]] = []
/// H5 WVJB.js postMessage `{bridgeReady:true}` true flush
private var bridgeReady = false
/// WebView + `WVJBHandler`
public init(webView: WKWebView,
controller: WKUserContentController,
@@ -52,7 +128,7 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
public func register(_ name: String, handler: @escaping BridgeHandler) {
handlers[name] = handler
bridgeLog.debug("[\(self.label, privacy: .public)] register handler '\(name, privacy: .public)'")
diagLog("[\(self.label)] register handler '\(name)'")
}
public func call(_ name: String, data: BridgeData?, callback: BridgeCallback?) {
@@ -66,7 +142,10 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
pendingCallbacks[cbId] = callback
payload["callbackId"] = cbId
}
bridgeLog.debug("[\(self.label, privacy: .public)] → H5 callHandler '\(name, privacy: .public)'")
// / / msext
let wire = (try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]))
.flatMap { String(data: $0, encoding: .utf8) } ?? "<encode-failed>"
diagLog("[\(self.label)] → H5 callHandler '\(name)' wire=\(wire)")
sendToJS(payload: payload)
}
@@ -87,6 +166,27 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
// MARK: -
private func handleIncoming(_ msg: [String: Any]) {
// H5 WVJB.js msext `__bridge_loaded__`
// user script flush
if msg["bridgeReady"] as? Bool == true {
bridgeReady = true
let queued = startupMessageQueue
startupMessageQueue.removeAll()
if !queued.isEmpty {
diagLog("[\(self.label)] 桥就绪,flush \(queued.count) 条暂存消息")
}
for payload in queued { dispatchToJS(payload: payload) }
return
}
// H5 handlerhandler
if let dispatched = msg["diagDispatched"] as? String {
let had = msg["hadHandler"] as? Bool ?? false
let threw = msg["threw"] as? String
diagLog("[\(self.label)] H5 派发 '\(dispatched)'hadHandler=\(had) threw=\(threw ?? "nil")")
return
}
// responseIdJS Native callHandler
if let responseId = msg["responseId"] as? String {
let respData = msg["responseData"].flatMap { BridgeData(jsonObject: $0) }
@@ -109,11 +209,19 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
// handler responseData JS
let responseCallback: BridgeCallback? = makeResponseCallback(for: callbackId)
bridgeLog.debug("[\(self.label, privacy: .public)] ← H5 call '\(name, privacy: .public)' data=\(String(describing: msg["data"]), privacy: .public)")
diagLog("[\(self.label)] ← H5 call '\(name)' data=\(String(describing: msg["data"]))")
guard let handler = handlers[name] else {
bridgeLog.error("[\(self.label, privacy: .public)] no NATIVE handler registered for '\(name, privacy: .public)'")
responseCallback?(nil)
// msext `WebViewJavascriptBridgeBase.flushMessageQueue`
// if (!handler) { NSLog(@"WVJBNoHandlerException, ..."); continue; }
// `continue` **responseCallback **
//
// `responseCallback?(nil)` H5 {responseId:...}
// H5 `Func.getlocation()` callback `getlocationinfo`
// `bridge.callHandler('getlocationinfo',"",function(resp){})`
// `getlocationinfo` handler
// JS
diagLog("[\(self.label)] no NATIVE handler registered for '\(name)'msext 同款:不回 responseCallback")
return
}
@@ -140,31 +248,41 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
sendToJS(payload: payload)
}
/// msext `WebViewJavascriptBridgeBase._queueMessage:`
/// `startupMessageQueue`
private func sendToJS(payload: [String: Any]) {
guard bridgeReady else {
let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
diagLog("[\(self.label)] '\(what)' 桥未就绪 → 入队(msext startupMessageQueue 等价,队列长 \(startupMessageQueue.count + 1)")
startupMessageQueue.append(payload)
return
}
dispatchToJS(payload: payload)
}
/// msext `_dispatchMessage:` `_handleMessageFromObjC`
/// H5 `setTimeout` msext dispatchMessagesWithTimeoutSafety=true
/// evaluateJavaScript **** H5 handler
private func dispatchToJS(payload: [String: Any]) {
guard let webView else {
bridgeLog.error("[\(self.label, privacy: .public)] sendToJS 丢弃:webView 已释放")
diagLog("[\(self.label)] dispatchToJS 丢弃:webView 已释放")
return
}
guard let data = try? JSONSerialization.data(withJSONObject: payload),
let json = String(data: data, encoding: .utf8)
else {
bridgeLog.error("[\(self.label, privacy: .public)] JSON encode failed for payload: \(String(describing: payload), privacy: .public)")
diagLog("[\(self.label)] JSON encode failed for payload: \(String(describing: payload))")
return
}
// base64 JSON / JS
// msext escape ///U+2028/U+2029
let base64 = Data(json.utf8).base64EncodedString()
// 'ok' / 'no-bridge' 便H5 bridge
let js = """
(function(){ if (!window.\(Self.jsBridgeName)) { return 'no-bridge'; } \
window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)'); return 'ok'; })()
"""
let js = "window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)');"
let tag = label
let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
webView.evaluateJavaScript(js) { result, error in
webView.evaluateJavaScript(js) { _, error in
if let error {
bridgeLog.error("[\(tag, privacy: .public)] sendToJS '\(what, privacy: .public)' evaluateJavaScript 失败: \(error.localizedDescription, privacy: .public)")
} else if (result as? String) != "ok" {
bridgeLog.error("[\(tag, privacy: .public)] sendToJS '\(what, privacy: .public)' 被丢弃:H5 侧 window.\(Self.jsBridgeName) 不存在")
diagLog("[\(tag)] dispatchToJS '\(what)' evaluateJavaScript 失败: \(error.localizedDescription)")
}
}
}
+15 -2
View File
@@ -43,6 +43,14 @@ public typealias BridgeCallback = @Sendable (BridgeData?) -> Void
public enum BridgeData: Sendable {
case string(String)
case number(Double)
/// JSON ****
///
/// `.number(Double)``JSONSerialization` Double 17
/// `28.636486` `28.636486000000001` JS `JSON.parse`
/// IEEE754 doubleH5 JSON ****
/// `getlocationinfo` msext UIWebView
/// `RootVC.m:1998` `\"latitude\":%f`
case decimal(Decimal)
case bool(Bool)
case null
case array([BridgeData])
@@ -89,6 +97,7 @@ extension BridgeData {
switch self {
case .string(let s): return s
case .number(let d): return d
case .decimal(let d): return d as NSDecimalNumber
case .bool(let b): return b
case .null: return NSNull()
case .array(let arr): return arr.map { $0.jsonObject }
@@ -124,6 +133,7 @@ extension BridgeData {
return String(Int(d))
}
return String(d)
case .decimal(let d): return "\(d)"
case .bool(let b): return b ? "true" : "false"
case .null: return nil
case .array, .object: return nil
@@ -131,8 +141,11 @@ extension BridgeData {
}
nonisolated public var asDouble: Double? {
if case .number(let d) = self { return d }
return nil
switch self {
case .number(let d): return d
case .decimal(let d): return (d as NSDecimalNumber).doubleValue
default: return nil
}
}
nonisolated public var asInt: Int? {
+5 -5
View File
@@ -47,25 +47,25 @@ public final class H5ErrorRelay: NSObject {
switch kind {
case "console.error":
let args = (dict["args"] as? [String]) ?? []
print("[H5 console.error]", args.joined(separator: " "))
diagLog("[H5 console.error] " + args.joined(separator: " "))
case "console.warn":
// WebViewJavascriptBridge.js Native handlerName
// H5 console.warn
let args = (dict["args"] as? [String]) ?? []
print("[H5 console.warn]", args.joined(separator: " "))
diagLog("[H5 console.warn] " + args.joined(separator: " "))
case "onerror":
let msg = (dict["msg"] as? String) ?? ""
let src = (dict["src"] as? String) ?? ""
let line = (dict["line"] as? Int) ?? 0
let col = (dict["col"] as? Int) ?? 0
let stack = dict["stack"] as? String
print("[H5 onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
diagLog("[H5 onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
case "unhandledrejection":
let reason = (dict["reason"] as? String) ?? ""
let stack = dict["stack"] as? String
print("[H5 unhandledrejection]", reason + (stack.map { "\n\($0)" } ?? ""))
diagLog("[H5 unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? ""))
default:
print("[H5 unknown error]", dict)
diagLog("[H5 unknown error] \(dict)")
}
}
@@ -21,7 +21,7 @@
//
// Phase 4.E SDK2.x static delegate API handleOpenURL/sendAuthReq
// SDK WeChatManager singleton pop
// Phase 5 `LocationService.shared.stop()`canImport
// Phase 5 `location.stop()`canImport
// Xcode Embed AMapLocationKit
// Phase 3.B/C/D / `AudioRecorder.shared.cancel()` opencore-amr
// `QiniuUploader.cancelInFlight()` Task.cancel
@@ -34,7 +34,9 @@ import Foundation
public enum BackGameDataHandler {
public static func register(on bridge: any BridgeProtocol) {
/// - Parameter location: **** LocationServicemsext cleanUpAction
/// gameController manager
public static func register(on bridge: any BridgeProtocol, location: LocationService) {
// 18backgameData
// data H5 getWebdata
// cb: "backgameData"
@@ -57,7 +59,7 @@ public enum BackGameDataHandler {
#if canImport(AMapLocationKit)
// msext gameController.m:2473-2478 cleanUpAction
LocationService.shared.stop(caller: "backgameData")
location.stop(caller: "backgameData")
#endif
// Phase 3.C / Phase 3.B QiniuUploader.cancelInFlight
@@ -25,46 +25,74 @@ private nonisolated let startLocLog = Logger(subsystem: "ylgamehall", category:
public enum StartLocationHandler {
/// - Parameter label: `lobby` / `subGame`
public static func register(on bridge: any BridgeProtocol, label: String = "?") {
/// - Parameters:
/// - label: `lobby` / `subGame`
/// - location: **** LocationServicemsext /
/// `AMapLocationManager` LocationService.init
public static func register(on bridge: any BridgeProtocol,
label: String = "?",
location: LocationService) {
bridge.register("startlocation") { data, callback in
// msext cb
callback?(.string("startlocation"))
startLocLog.debug("[\(label, privacy: .public)] ← startlocation 入参 type=\(data?.asLooseString ?? "nil", privacy: .public)")
diagLog("[\(label)] ← startlocation 入参 type=\(data?.asLooseString ?? "nil")")
#if canImport(AMapLocationKit)
// SDK callback 9
// data == 1 Phase 5.4
// msext NewRootVC.m:411-421 / gameController.m:533-543
// int tempinfo = [data intValue];
// if (tempinfo == 1) [self.locationManager startUpdatingLocation]; //
// else [self reGeocodeAction]; //
// H5 gamehall / jinxianmahjong 2
// msext msext
let isContinuous = Int(data?.asLooseString ?? "") == 1
Task { @MainActor in
do {
let payload = try await LocationService.shared.requestOnce(caller: label)
startLocLog.debug("[\(label, privacy: .public)] → getlocationinfo 成功 city=\(payload.city, privacy: .public) province=\(payload.province, privacy: .public)")
bridge.call("getlocationinfo", data: .object([
"address": .string(payload.address),
"city": .string(payload.city),
"cityCode": .string(payload.cityCode),
"country": .string(payload.country),
"district": .string(payload.district),
"latitude": .string(payload.latitude), // string
"longitude": .string(payload.longitude), // string
"province": .string(payload.province), // p
"street": .string(payload.street)
]), callback: nil)
} catch LocationError.authorizationDenied {
// msext gameController.m:2507
startLocLog.error("[\(label, privacy: .public)] → getlocationinfo 失败:authorizationDenied")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
} catch {
// 12msext
startLocLog.error("[\(label, privacy: .public)] → getlocationinfo 失败:\(String(describing: error), privacy: .public)")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
// outcomemsext completionBlock
// .success
let dispatch: @MainActor (LocationOutcome) -> Void = { outcome in
switch outcome {
case .failure:
// msext gameController.m:2507
diagLog("[\(label)] → getlocationinfo errorCode=12")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
case .success(let payload):
// msext gameController.m:2528 9 + `errorCode: 0`
//
// docs/H5-Native-Contract.md §3.2 B
// msext **** `errorCode: 12`
// 9 **** errorCode
// H5 `errorCode == 0` ""
// jinxianmahjong 05_Func.js2026-07-19
// `Func.startlocation` / `Func.getlocation`
// `"errorCode":0` gamehall
// 05_Func.js2026-02-04 errorCode
// ""
// errorCode H5
// **** msext
//
diagLog("[\(label)] → getlocationinfo 成功 city=\(payload.city) province=\(payload.province)")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(0),
"address": .string(payload.address),
"city": .string(payload.city),
"cityCode": .string(payload.cityCode),
"country": .string(payload.country),
"district": .string(payload.district),
"latitude": .decimal(payload.latitude), // 6
"longitude": .decimal(payload.longitude), // 6
"province": .string(payload.province), // p
"street": .string(payload.street)
]), callback: nil)
}
}
if isContinuous {
location.startContinuous(caller: label, emit: dispatch)
} else {
location.requestOnce(caller: label, emit: dispatch)
}
}
#endif
@@ -90,6 +90,7 @@ public final class AppCoordinator {
AudioPlayer.shared.stopAllBackground()
let subGame = SubGameViewController(request: request)
diagLog("[coordinator] push 子游戏(栈深 \(nav.viewControllers.count)\(nav.viewControllers.count + 1)")
nav.pushViewController(subGame, animated: true)
return true
}
@@ -97,6 +98,7 @@ public final class AppCoordinator {
/// pop data H5 backgameData
/// WebContainer `getWebdata` callback
public func popSubGame(returningData data: String) {
diagLog("[coordinator] pop 子游戏回大厅")
navigationController?.popViewController(animated: true)
NotificationCenter.default.post(
name: .subGameDidReturn,
+224 -41
View File
@@ -32,93 +32,276 @@ public struct LocationPayload: Sendable {
public let cityCode: String
public let country: String
public let district: String
public let latitude: String
public let longitude: String
/// **JSON ** `%f` 6
/// `Decimal` `Double` init §3.2 B
public let latitude: Decimal
public let longitude: Decimal
public let province: String
public let street: String
}
public enum LocationError: Error, Sendable {
case sdkNotLinked
case authorizationDenied
case timeout
case underlying(any Error)
#if canImport(AMapLocationKit)
extension LocationPayload {
/// msext `@{...}`
///
/// msext
/// ```objc
/// @try{ [_bridge callHandler:@"getlocationinfo" data:@{
/// @"address":regeocode.formattedAddress, @"city":regeocode.city, ... }]; }
/// @catch (NSException * e) { NSLog(...); }
/// ```
/// `@{}` ** value nil NSException**
/// `@try/@catch` 9 nil
/// ** getlocationinfo H5**
///
/// `?? ""`
/// H5 msext
/// nil nil
nonisolated init?(location: CLLocation, reGeocode: AMapLocationReGeocode) {
guard let address = reGeocode.formattedAddress,
let city = reGeocode.city,
let cityCode = reGeocode.citycode,
let country = reGeocode.country,
let district = reGeocode.district,
let province = reGeocode.province,
let street = reGeocode.street
else { return nil }
self.address = address
self.city = city
self.cityCode = cityCode
self.country = country
self.district = district
// **** `%f` 6
//
// docs/H5-Native-Contract.md §3.2 Bmsext WVJB
// `gameController.m:2528` / `NewRootVC.m:2132`
// `[NSString stringWithFormat:@"%f", ...]` ****
// UIWebView / JSContext `RootVC.m:1998` / `fourviewVC.m:1541`
// `\"latitude\":%f` ** JSON ** H5
// jinxianmahjong 05_Func.js:2029-2031
// `"latitude":28.623546` `errorCode` WVJB
//
//
// `%f` `Decimal` 6
// JSONSerialization `28.636486` Double 17
// `28.636486000000001` JS parse IEEE754 doubleH5
// JSON
let latText = String(format: "%f", location.coordinate.latitude)
let lonText = String(format: "%f", location.coordinate.longitude)
self.latitude = Decimal(string: latText) ?? Decimal(location.coordinate.latitude)
self.longitude = Decimal(string: lonText) ?? Decimal(location.coordinate.longitude)
self.province = province
self.street = street
}
}
#endif
/// **** failure success
/// msext `completionBlock` requestOnce
public enum LocationOutcome: Sendable {
/// H5 `getlocationinfo({errorCode:12, errorMsg:""})`
case failure
/// H5 `getlocationinfo(<9 >)`
case success(LocationPayload)
}
@MainActor
public final class LocationService {
public static let shared = LocationService()
/// `lobby` / `subGame`
private let owner: String
public init() {}
/// ****
/// msext `NewRootVC.m:1568` / `gameController.m:1230`
/// `[[AMapLocationManager alloc] init]` manager + completionBlock +
/// delegate manager
/// 1. `requestLocationWithReGeocode:completionBlock:`
///
/// completionBlock continuation H5
/// 2. `stopUpdatingLocation` cancel
/// `backgameData`
/// 3. `delegate = nil` manager ****
public init(owner: String = "?") {
self.owner = owner
#if canImport(AMapLocationKit)
// msext `configLocationManager` `setDelegate:self` ****
// `amapLocationManager:doRequireLocationAuth:` delegate
// init shim lazy
_ = delegateShim
#endif
}
#if canImport(AMapLocationKit)
/// msext `configLocationManager`
private let manager: AMapLocationManager = {
let m = AMapLocationManager()
m.desiredAccuracy = kCLLocationAccuracyHundredMeters
m.pausesLocationUpdatesAutomatically = false
m.locationTimeout = 6
m.reGeocodeTimeout = 3
m.locatingWithReGeocode = true
return m
}()
/// `startlocation` type == 1 delegate
/// msext `configLocationManager` `setDelegate:self` VC
/// LocationService NSObject
private lazy var delegateShim: LocationDelegateShim = {
let shim = LocationDelegateShim(owner: owner)
manager.delegate = shim
return shim
}()
#endif
/// requestOnce
private var requestSeq: UInt64 = 0
/// + msext locAction + completionBlock
public func requestOnce(caller: String = "?") async throws -> LocationPayload {
/// + ** msext `initCompleteBlock` completionBlock**
/// `gameController.m:2493-2545` / `NewRootVC.m:2099-2150`
///
/// ```objc
/// if (error) {
/// callHandler getlocationinfo {errorCode:12,...} //
/// if (error.code == AMapLocationErrorLocateFailed) return;
/// }
/// if (location) { if (regeocode) {
/// if (regeocode.formattedAddress != nil)
/// callHandler getlocationinfo <9 > //
/// }}
/// ```
///
/// ** return** locateFailed CLLocation
/// / error **** location msext
/// errorCode 12H5
///
/// `async throws -> LocationPayload`
/// error return H5 errorCode 12
/// `C_Player.SetLocationInfo({errorCode:12})` addr
/// /CPU
/// msext outcome
///
/// - Parameter emit: MainActor 0 / 1 / 2
public func requestOnce(caller: String = "?",
emit: @escaping @MainActor (LocationOutcome) -> Void) {
#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)")
diagLog("[\(caller)] requestOnce #\(seq) 发起(owner=\(owner) manager=\(UInt(bitPattern: ObjectIdentifier(self.manager).hashValue))")
let t0 = Date()
// completionBlock 线 msext
let accepted = manager.requestLocation(withReGeocode: true) { loc, regeo, err in
let ms = Int(Date().timeIntervalSince(t0) * 1000)
diagLog("[\(caller)] requestOnce #\(seq) completionBlock 回来(耗时 \(ms)ms):loc=\(loc != nil) regeo=\(regeo != nil) err=\(err?.localizedDescription ?? "nil")")
MainActor.assumeIsolated {
if let err = err as NSError? {
// msext 12 errorCodehandler
emit(.failure) //
if err.code == AMapLocationErrorCode.locateFailed.rawValue {
cont.resume(throwing: LocationError.authorizationDenied)
} else {
cont.resume(throwing: LocationError.underlying(err))
diagLog("[\(caller)] requestOnce #\(seq) locateFailed,按 msext 就此结束")
return
}
diagLog("[\(caller)] requestOnce #\(seq) 有 error 但非 locateFailed,按 msext 继续尝试补发真实定位")
}
// msext location / regeocode / formattedAddress
guard let loc, let regeo, regeo.formattedAddress != nil else {
diagLog("[\(caller)] requestOnce #\(seq) 无可用逆地理(loc=\(loc != nil) regeo=\(regeo != nil)),按 msext 不发 9 字段")
return
}
guard let loc, let regeo else {
cont.resume(throwing: LocationError.timeout)
guard let payload = LocationPayload(location: loc, reGeocode: regeo) else {
diagLog("[\(caller)] requestOnce #\(seq) 逆地理有 nil 字段,按 msext@{} 抛 NSException)整条不发")
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 ?? ""
))
emit(.success(payload)) //
}
// AMapLocationManager.hRequest NO
// completionBlock continuation H5
// getlocationinfo errorCode 12
locLog.debug("[\(caller, privacy: .public)] requestOnce #\(seq, privacy: .public) requestLocation 返回 accepted=\(accepted, privacy: .public)")
}
// AMapLocationManager.hRequestmsext
// H5 continuation
// 便
if !accepted {
diagLog("[\(caller)] requestOnce #\(seq) requestLocation 返回 NO(未挂上请求),按 msext 静默")
}
#else
locLog.error("[\(caller, privacy: .public)] requestOnce: AMapLocationKit 未链接,抛 sdkNotLinked")
throw LocationError.sdkNotLinked
diagLog("[\(caller)] requestOnce: AMapLocationKit 未链接,按 msext 无 SDK 场景不回 H5")
#endif
}
/// `startlocation` type == 1
/// msext `NewRootVC.m:414` / `gameController.m:537`
/// `if (tempinfo == 1) [self.locationManager startUpdatingLocation];`
/// delegate `amapLocationManager:didUpdateLocation:reGeocode:`
/// `getlocationinfo`msext `gameController.m:2551-2560` `reGeocode`
/// `formattedAddress` `didFailWithError` msext NSLog H5
///
/// - Parameter emit:
public func startContinuous(caller: String = "?",
emit: @escaping @MainActor (LocationOutcome) -> Void) {
#if canImport(AMapLocationKit)
diagLog("[\(caller)] startContinuous 发起(owner=\(owner)")
delegateShim.onUpdate = emit
delegateShim.caller = caller
manager.startUpdatingLocation()
#else
diagLog("[\(caller)] startContinuous: AMapLocationKit 未链接,按 msext 无 SDK 场景不回 H5")
#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(会取消所有在飞的单次定位)")
// `stopUpdatingLocation` cancel manager
// manager msext
// cleanUpAction manager
diagLog("[\(caller)] stop()stopUpdatingLocation + delegate=nilowner=\(owner)")
manager.stopUpdatingLocation()
manager.delegate = nil
#endif
}
}
#if canImport(AMapLocationKit)
/// `AMapLocationManagerDelegate`
/// msext NewRootVC / gameController
/// LocationService 宿
@MainActor
private final class LocationDelegateShim: NSObject, AMapLocationManagerDelegate {
var onUpdate: (@MainActor (LocationOutcome) -> Void)?
var caller: String = "?"
private let owner: String
init(owner: String) {
self.owner = owner
super.init()
}
/// msext `gameController.m:2551-2560` reGeocode formattedAddress
/// 9
nonisolated func amapLocationManager(_ manager: AMapLocationManager!,
didUpdate location: CLLocation!,
reGeocode: AMapLocationReGeocode!) {
// AMap Sendable Sendable
// LocationPayload MainActor Swift 6 reGeocode
guard let location, let reGeocode, reGeocode.formattedAddress != nil else {
diagLog("[持续定位] 更新但无可用逆地理,按 msext 不发")
return
}
guard let payload = LocationPayload(location: location, reGeocode: reGeocode) else {
diagLog("[持续定位] 逆地理有 nil 字段,按 msext@{} 抛 NSException)整条不发")
return
}
MainActor.assumeIsolated {
diagLog("[\(caller)] 持续定位 → getlocationinfo city=\(payload.city)")
onUpdate?(.success(payload))
}
}
/// msext `gameController.m:2547` ** H5**
nonisolated func amapLocationManager(_ manager: AMapLocationManager!,
didFailWithError error: (any Error)!) {
diagLog("[持续定位 owner=\(owner)] didFailWithError: \(error?.localizedDescription ?? "nil")msext 同款:不通知 H5")
}
}
#endif
@@ -34,6 +34,10 @@ public final class SubGameViewController: UIViewController {
// MARK: - UI
private let bridgedWebView = BridgedWebView(label: "subGame")
/// msext gameController.m:1230 `configLocationManager`
/// alloc AMapLocationManager VC
private let locationService = LocationService(owner: "subGame")
private let splash = SplashOverlay()
// MARK: - Handlers
@@ -88,7 +92,7 @@ public final class SubGameViewController: UIViewController {
DeviceInfoHandler.register(on: bridge)
BrowserHandler.register(on: bridge)
OpenSaomaHandler.register(on: bridge)
StartLocationHandler.register(on: bridge, label: "subGame")
StartLocationHandler.register(on: bridge, label: "subGame", location: locationService)
// assetsRoot = H5 msext gameController.m:364
// closure effectiveGameDir boot pipeline
@@ -118,7 +122,7 @@ public final class SubGameViewController: UIViewController {
OpenurlTitleDataHandler.register(on: bridge)
// backgameData退 + callback getWebdata
BackGameDataHandler.register(on: bridge)
BackGameDataHandler.register(on: bridge, location: locationService)
// 3 stub H5 "no handler"
// Agora VideoRoomHandlers 3 stub
@@ -16,6 +16,10 @@ public final class WebContainerViewController: UIViewController {
// MARK: - UI
private let bridgedWebView = BridgedWebView(label: "lobby")
/// msext NewRootVC.m:1568 `configLocationManager`
/// msext
private let locationService = LocationService(owner: "lobby")
private let splash = SplashOverlay()
// MARK: - Handlers ( handler enum register)
@@ -77,7 +81,7 @@ public final class WebContainerViewController: UIViewController {
DeviceInfoHandler.register(on: bridge) // §3.1 21 + §3.2 1
BrowserHandler.register(on: bridge) // §3.1 16
OpenSaomaHandler.register(on: bridge) // §3.1 22 stub
StartLocationHandler.register(on: bridge, label: "lobby") // §3.1 20Phase 5 Phase 2 stub
StartLocationHandler.register(on: bridge, label: "lobby", location: locationService) // §3.1 20Phase 5 Phase 2 stub
// Phase 3.A + 3.C/3.D stub
// assetsRoot = H5 lobbyIndex lobby