// // 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) 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 } }