闭合 Phase 1.17 联调最后一公里(解决 Xcode 日志 4 个 "no handler registered"
+ 提前落地 4 个无外部依赖的简单 handler)。
新增 7 个 handler 模块(Source/Bridge/Handlers/):
- ClipboardHandler.swift §3.1 [13][14]剪贴板 UIPasteboard.general
- ShakeHandler.swift §3.1 [7][8][9]摇一摇 + §3.2 [5]shakeEnd 反向
canShake / canVoice 状态 + motionEnded 转发钩子
- VoicePlayingHandler.swift §3.1 [6]actor VoiceCenter.shared 开关位
- DeviceInfoHandler.swift §3.1 [21]+ §3.2 [1]getphoneInfo 大写 I 入、
getphoneinfo 小写 i 出反向 callback
DeviceInfoSnapshot 6 字段(IDFA 用 idfv 兜底,
CLAUDE.md 不接 ATT/IDFA)
- BrowserHandler.swift §3.1 [16]UIApplication.open async(Task @MainActor
异步打开,闭包先 cb 不等 open 完成)
- OpenSaomaHandler.swift §3.1 [22]空 stub(契约要求注册)
- StartLocationHandler.swift §3.1 [20]Phase 5 完整实现,Phase 2 stub
扩展 VibratorHandler.swift:
- §3.1 [10]vibrator(原有)+ [11]repeatvibrator(msext RootVC.m:1820 沿用:
实际单次 vibrate 不真循环)+ [12]canclevibrator
BridgeProtocol.swift BridgeData 加 nonisolated:
- 项目默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 让 BridgeData 跨 actor
访问被拦;asString / asDouble / asInt / asBool / asArray / asObject /
subscript 全部标 nonisolated(纯值类型本就该是 isolation-free)
- 影响所有 @Sendable async handler 闭包内的 data?.asXxx 访问
WebContainerViewController 接入:
- 加 private let shakeHandler = ShakeHandler() 持有
- registerBridgeHandlers() 注册 8 个 handler 模块
- 加 canBecomeFirstResponder = true + viewDidAppear becomeFirstResponder
- 加 motionEnded(_:with:) 转发到 shakeHandler.handleMotionEnded
(motion 检测必需 first responder,否则不触发)
BuildProject 通过。重装 app 后 H5 启动应不再报 "no handler registered" 错误。
Plan §5 Phase 2.A 全部勾选(含 startlocation bonus stub + BridgeData
nonisolated 重构)。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
70 lines
2.8 KiB
Swift
70 lines
2.8 KiB
Swift
//
|
||
// DeviceInfoHandler.swift
|
||
// ylgamehall
|
||
//
|
||
// H5 → Native handler:getphoneInfo + 反向 callback getphoneinfo(小写 i)
|
||
// 契约:docs/H5-Native-Contract.md §3.1 [21] + §3.2 [1] + 表 A
|
||
//
|
||
// ⚠️ 大小写硬约束:
|
||
// H5 调用:bridge.callHandler("getphoneInfo", ...) 大写 I
|
||
// 原生回调:bridge.callHandler("getphoneinfo", ...) 小写 i
|
||
//
|
||
|
||
import UIKit
|
||
|
||
@MainActor
|
||
public enum DeviceInfoHandler {
|
||
|
||
public static func register(on bridge: any BridgeProtocol) {
|
||
// 【21】getphoneInfo(大写 I)
|
||
// 入参忽略;cb:"getphoneInfo"
|
||
// 反向调用:bridge.call("getphoneinfo"(小写 i!),data: 表 A 6 字段)
|
||
bridge.register("getphoneInfo") { [weak bridge] _, callback in
|
||
callback?(.string("getphoneInfo"))
|
||
|
||
Task { @MainActor in
|
||
let snapshot = DeviceInfoSnapshot.current()
|
||
bridge?.call("getphoneinfo", // ⚠️ 小写 i
|
||
data: .object(snapshot.bridgeFields),
|
||
callback: nil)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 设备信息快照(契约 §3.2 表 A 6 字段,全部 string)。
|
||
@MainActor
|
||
public struct DeviceInfoSnapshot {
|
||
public let phoneAdresseMAC: String // UIDevice.identifierForVendor.uuidString
|
||
public let phoneDeviceBrand: String // "iPhone" / "iPad"
|
||
public let phoneIMEI: String // 历史用 IDFA;新外壳暂用 idfv(IDFA 需 ATT,本项目不接)
|
||
public let phoneModel: String // UIDevice.localizedModel
|
||
public let phoneProvidersName: String // 运营商名(iOS 16+ 私有 / 已弃用,暂空)
|
||
public let phoneVersion: String // UIDevice.systemVersion
|
||
|
||
public static func current() -> DeviceInfoSnapshot {
|
||
let device = UIDevice.current
|
||
let idfv = device.identifierForVendor?.uuidString ?? ""
|
||
return DeviceInfoSnapshot(
|
||
phoneAdresseMAC: idfv,
|
||
phoneDeviceBrand: device.model,
|
||
phoneIMEI: idfv, // CLAUDE.md 不接 ATT/IDFA,沿用 idfv
|
||
phoneModel: device.localizedModel,
|
||
phoneProvidersName: "", // iOS 16+ CTCarrier 已弃用,业务暂用不到
|
||
phoneVersion: device.systemVersion
|
||
)
|
||
}
|
||
|
||
/// 契约 §3.2 表 A 字段名严格保持
|
||
public var bridgeFields: [String: BridgeData] {
|
||
[
|
||
"PhoneAdresseMAC": .string(phoneAdresseMAC),
|
||
"PhoneDeviceBrand": .string(phoneDeviceBrand),
|
||
"PhoneIMEI": .string(phoneIMEI),
|
||
"PhoneModel": .string(phoneModel),
|
||
"PhoneProvidersName": .string(phoneProvidersName),
|
||
"PhoneVersion": .string(phoneVersion)
|
||
]
|
||
}
|
||
}
|