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:
co-authored by
Claude Opus 4.7
parent
c6221d30ca
commit
d48ecc0473
@@ -2643,6 +2643,202 @@ public final class QQShareManager: NSObject {
|
|||||||
|
|
||||||
→ 上层 `accreditLogin` / `friendsShare...` handler 看不到这些细节,只看到 `try await`,代码大厅 / 子游戏完全一致。
|
→ 上层 `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. 并发模型
|
## 9. 并发模型
|
||||||
|
|||||||
Reference in New Issue
Block a user