diff --git a/docs/H5-Native-Implementation-Design.md b/docs/H5-Native-Implementation-Design.md index 055b03a..e06d47f 100644 --- a/docs/H5-Native-Implementation-Design.md +++ b/docs/H5-Native-Implementation-Design.md @@ -2643,6 +2643,202 @@ public final class QQShareManager: NSObject { → 上层 `accreditLogin` / `friendsShare...` handler 看不到这些细节,只看到 `try await`,代码大厅 / 子游戏完全一致。 +### 8.6 LocationKit + +覆盖契约 §F §3.1 [20]`startlocation` + §3.2 [6]`getlocationinfo` 反向 callback 共 2 项接口。 + +#### 8.6.1 Module 骨架 + +依赖高德 `AMapLocationKit.xcframework`(ADR-006 走 Vendor 手动接入),用 Swift wrapper 隔离 Objective-C SDK,避免业务层接触 AMap 类型。 + +```swift +// Source/LocationKit/LocationService.swift +import AMapLocationKit + +@MainActor +public final class LocationService: NSObject, AMapLocationManagerDelegate { + + /// 启动期完成 AMap privacy + key 配置(详见 §4.2 启动并行任务) + public static func bootstrap() { + AMapServices.shared().enableHTTPS = true + AMapServices.shared().apiKey = BundleConfig.shared.amapKey + AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain) + AMapLocationManager.updatePrivacyAgree(.didAgree) + } + + private let manager: AMapLocationManager = { + let m = AMapLocationManager() + m.desiredAccuracy = kCLLocationAccuracyHundredMeters + m.locationTimeout = 6 + m.reGeocodeTimeout = 4 + m.locatingWithReGeocode = true + return m + }() + + private var continuousActive = false + + public override init() { + super.init() + manager.delegate = self + } + + // ─── 一次性定位(startlocation 入参 ≠ 1)────────────── + public func locateOnce() async throws -> LocationInfo { + try await withCheckedThrowingContinuation { cont in + manager.requestLocation(withReGeocode: true) { loc, reGeo, error in + if let error = error { + cont.resume(throwing: error) + return + } + guard let loc = loc, let reGeo = reGeo else { + cont.resume(throwing: LocationError.empty) + return + } + cont.resume(returning: LocationInfo(coord: loc, reGeo: reGeo)) + } + } + } + + // ─── 持续定位(startlocation 入参 == 1)───────────────── + public var onContinuousUpdate: ((LocationInfo) -> Void)? + + public func startContinuous() { + guard !continuousActive else { return } + continuousActive = true + manager.startUpdatingLocation() + } + + public func stopContinuous() { + guard continuousActive else { return } + continuousActive = false + manager.stopUpdatingLocation() + } + + public func amapLocationManager(_ manager: AMapLocationManager!, + didUpdate location: CLLocation!, + reGeocode: AMapLocationReGeocode!) { + guard let loc = location, let reGeo = reGeocode else { return } + onContinuousUpdate?(LocationInfo(coord: loc, reGeo: reGeo)) + } +} + +public struct LocationInfo: 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 // ⚠️ string,契约硬约束(stringWithFormat:@"%f") + public let longitude: String // ⚠️ string + public let province: String // ⚠️ 小写 p,与 sharelogin 大写 P 不一致,沿用历史 + public let street: String + + init(coord: CLLocation, reGeo: AMapLocationReGeocode) { + address = reGeo.formattedAddress ?? "" + city = reGeo.city ?? "" + cityCode = reGeo.citycode ?? "" + country = reGeo.country ?? "" + district = reGeo.district ?? "" + latitude = String(format: "%f", coord.coordinate.latitude) + longitude = String(format: "%f", coord.coordinate.longitude) + province = reGeo.province ?? "" + street = reGeo.street ?? "" + } +} + +public enum LocationError: Error, Sendable { + case empty + case permissionDenied(code: Int = 12) +} +``` + +#### 8.6.2 H5 location handler 实现骨架 + +```swift +// Source/Bridge/Handlers/LocationHandlers.swift +@MainActor +public struct LocationHandlers { + + let bridge: BridgeProtocol + let service: LocationService + + public func register() { + bridge.register("startlocation", handler: startLocation) + } + + // MARK: - 【20】 startlocation + // + // 契约 §3.1 [20]: + // 入参 data : int 1=持续 startUpdatingLocation + // 其他=一次性 reGeocodeAction + // responseCallback: "startlocation" + // 反向 callback : getlocationinfo(§3.2 [6]) + // 成功: 表 B 9 字段(latitude/longitude 是 string, + // province 小写 p) + // 失败: { errorCode: 12, errorMsg: "缺少定位权限" } + // ⚠️ errorCode 是数字(NSNumber)不是 string + // + // 持续模式下,service.onContinuousUpdate 在 register() 时挂钩; + // 一次性模式直接 await + 推一次 + + private func startLocation(_ data: BridgeData?, _ cb: BridgeCallback?) async { + defer { cb?(.string("startlocation")) } + let continuous = (data?.asInt ?? 0) == 1 + + // 持续模式:挂钩 + 启动 → 每次位置变化都推 getlocationinfo + if continuous { + service.onContinuousUpdate = { [weak bridge] info in + bridge?.call("getlocationinfo", data: info.bridgeData, callback: nil) + } + service.startContinuous() + return + } + + // 一次性:await + 单次推 getlocationinfo + do { + let info = try await service.locateOnce() + bridge.call("getlocationinfo", data: info.bridgeData, callback: nil) + } catch { + // 契约 §3.2 表 B 失败结构:errorCode 是数字(不是 string) + bridge.call("getlocationinfo", + data: .object([ + "errorCode": .number(12), + "errorMsg": .string("缺少定位权限") + ]), + callback: nil) + } + } +} + +extension LocationInfo { + /// 转成契约 §3.2 表 B 的 BridgeData 结构(所有字段 string) + var bridgeData: BridgeData { + .object([ + "address": .string(address), + "city": .string(city), + "cityCode": .string(cityCode), + "country": .string(country), + "district": .string(district), + "latitude": .string(latitude), // stringified + "longitude": .string(longitude), // stringified + "province": .string(province), // 小写 p + "street": .string(street) + ]) + } +} +``` + +#### 8.6.3 与原 msext 的差异 + +| 维度 | msext 现状(不要照抄) | 新外壳决策 | +|------|--------------------|----------| +| SDK 接入 | CocoaPods `AMapLocation` 包含的 framework + Reachability headers 多重耦合 | Vendor 手动 `AMapLocationKit.xcframework`(ADR-006),SPM/CocoaPods 双关 | +| 隐私合规 | `updatePrivacyShow:.didShow` / `updatePrivacyAgree:.didAgree` 在 AppDelegate 散落 | `LocationService.bootstrap()` 集中调用 | +| 回调风格 | `delegate amapLocationManager:didUpdateLocation:reGeocode:` 散落赋值给 self | `async/await` + `onContinuousUpdate` 闭包,actor 安全 | +| 持续/一次性入参 | 入参 string 类型,`[data intValue]` 转 int 后判断 | BridgeData.asInt 直接拿 | +| latitude 字段类型 | `stringWithFormat:@"%f"` 字符串 | `String(format: "%f", ...)`(等价) | +| province 大小写 | 小写 p(与 sharelogin 大写 P 不一致)| 严格保持 | + --- ## 9. 并发模型