Design §8.6:新增 LocationKit + 2 项接口实现骨架

覆盖 §3.1 [20]startlocation + §3.2 [6]getlocationinfo 反向 callback。

§8.6.1 LocationService module 骨架:
  - AMapServices.bootstrap:privacy + key 启动期完成
  - locateOnce() async/await 包装 AMapLocationManager.requestLocation
  - startContinuous / stopContinuous + onContinuousUpdate 闭包钩子
  - LocationInfo struct 转契约 §3.2 表 B 9 字段(latitude/longitude string
    化用 String(format: "%f"),province 小写 p)
  - LocationError.permissionDenied(code: 12)

§8.6.2 H5 handler(LocationHandlers.startLocation):
  - 入参 data: int 1=持续 / 其他=一次性,与 msext RootVC 沿用
  - 持续模式:onContinuousUpdate 挂钩 → 每次位置变化推 getlocationinfo
  - 一次性:await locateOnce → 单次推
  - responseCallback: "startlocation"
  - 失败 callback 数据结构 errorCode 数字 12 不是 string(契约硬约束)

§8.6.3 与 msext 6 维度差异表(SDK 接入 / 隐私合规集中 / async wrapper /
入参解析 / latitude string / province 小写 p)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 07:30:06 +08:00
co-authored by Claude Opus 4.7
parent c6221d30ca
commit d48ecc0473
+196
View File
@@ -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 // stringstringWithFormat:@"%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. 并发模型