闭合 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>
177 lines
6.0 KiB
Swift
177 lines
6.0 KiB
Swift
//
|
||
// BridgeProtocol.swift
|
||
// ylgamehall
|
||
//
|
||
// H5 ↔ Native 桥的核心抽象。详见 docs/H5-Native-Implementation-Design.md §3.2。
|
||
//
|
||
|
||
import Foundation
|
||
|
||
// MARK: - 桥接协议
|
||
|
||
/// 容器层(WebView VC)和 handler 注册层都依赖此抽象,便于单测注入 mock。
|
||
///
|
||
/// - 实现见 `BridgeBus`(@MainActor,挂 WKWebView 上)
|
||
/// - H5 端协议见 `Resources/JS/WebViewJavascriptBridge.js`(Phase 1.8 引入)
|
||
public protocol BridgeProtocol: AnyObject, Sendable {
|
||
|
||
/// 注册 H5 → Native handler。重复 name 后注册的会覆盖前一个。
|
||
func register(_ name: String, handler: @escaping BridgeHandler)
|
||
|
||
/// Native → H5 主动调用 H5 端 handler。`callback` 可选。
|
||
func call(_ name: String, data: BridgeData?, callback: BridgeCallback?)
|
||
}
|
||
|
||
/// H5 → Native handler 签名。
|
||
///
|
||
/// - 第 1 参:H5 端 `bridge.callHandler('name', data, ...)` 传过来的 data
|
||
/// - 第 2 参:响应回调,handler 调一次 `callback?(...)` 把结果回给 H5;可不调
|
||
/// - async:允许 handler 内部 await(如等待 SDK / 网络)
|
||
public typealias BridgeHandler = @Sendable (BridgeData?, BridgeCallback?) async -> Void
|
||
|
||
/// 桥回调签名(H5 → Native 的 responseCallback,或 Native → H5 的回包闭包)。
|
||
public typealias BridgeCallback = @Sendable (BridgeData?) -> Void
|
||
|
||
// MARK: - BridgeData
|
||
|
||
/// 桥消息载荷:与 JSON 值同构的代数数据类型。
|
||
///
|
||
/// 所有 H5 ↔ Native 之间流转的数据最终都序列化为此类型,方便:
|
||
/// - 类型化访问(`data["title"]?.asString` 比 `data["title"] as? String` 干净)
|
||
/// - Sendable 跨 actor 边界(避免 `[String: Any]` 这种 non-Sendable 类型扩散)
|
||
/// - 单测 fixture 构造(字面量语法 `["a": 1, "b": [true, nil]]` 等价 `.object([...])`)
|
||
public enum BridgeData: Sendable {
|
||
case string(String)
|
||
case number(Double)
|
||
case bool(Bool)
|
||
case null
|
||
case array([BridgeData])
|
||
case object([String: BridgeData])
|
||
}
|
||
|
||
// MARK: - JSON 互转
|
||
|
||
extension BridgeData {
|
||
|
||
/// 从 `JSONSerialization` 输出(Any 树)构造,递归处理子节点。
|
||
/// 遇到未知类型返回 nil(不静默丢弃,便于上层检测异常 payload)。
|
||
public init?(jsonObject: Any) {
|
||
if jsonObject is NSNull { self = .null; return }
|
||
if let s = jsonObject as? String { self = .string(s); return }
|
||
if let n = jsonObject as? NSNumber {
|
||
// NSNumber 同时桥接 Bool/Int/Double。Bool 必须先用 CFTypeID 鉴别,
|
||
// 否则 NSNumber(value: true) 会被当 1.0 处理。
|
||
if CFGetTypeID(n) == CFBooleanGetTypeID() {
|
||
self = .bool(n.boolValue)
|
||
return
|
||
}
|
||
self = .number(n.doubleValue)
|
||
return
|
||
}
|
||
if let arr = jsonObject as? [Any] {
|
||
self = .array(arr.compactMap { BridgeData(jsonObject: $0) })
|
||
return
|
||
}
|
||
if let obj = jsonObject as? [String: Any] {
|
||
var out: [String: BridgeData] = [:]
|
||
out.reserveCapacity(obj.count)
|
||
for (k, v) in obj {
|
||
if let d = BridgeData(jsonObject: v) { out[k] = d }
|
||
}
|
||
self = .object(out)
|
||
return
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// 转为 `JSONSerialization` 可接受的 Any 树,供 evaluateJavaScript 拼 JSON 字符串。
|
||
public var jsonObject: Any {
|
||
switch self {
|
||
case .string(let s): return s
|
||
case .number(let d): return d
|
||
case .bool(let b): return b
|
||
case .null: return NSNull()
|
||
case .array(let arr): return arr.map { $0.jsonObject }
|
||
case .object(let d): return d.mapValues { $0.jsonObject }
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 访问语法糖
|
||
// 注:项目默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 让所有类型默认 main 隔离;
|
||
// BridgeData 是纯值类型、需要在 handler 闭包(@Sendable)内自由访问,
|
||
// 所有访问语法糖标 nonisolated 避免跨 actor 限制。
|
||
|
||
extension BridgeData {
|
||
|
||
nonisolated public var asString: String? {
|
||
if case .string(let s) = self { return s }
|
||
return nil
|
||
}
|
||
|
||
nonisolated public var asDouble: Double? {
|
||
if case .number(let d) = self { return d }
|
||
return nil
|
||
}
|
||
|
||
nonisolated public var asInt: Int? {
|
||
if case .number(let d) = self { return Int(d) }
|
||
return nil
|
||
}
|
||
|
||
nonisolated public var asBool: Bool? {
|
||
if case .bool(let b) = self { return b }
|
||
return nil
|
||
}
|
||
|
||
nonisolated public var asArray: [BridgeData]? {
|
||
if case .array(let a) = self { return a }
|
||
return nil
|
||
}
|
||
|
||
nonisolated public var asObject: [String: BridgeData]? {
|
||
if case .object(let o) = self { return o }
|
||
return nil
|
||
}
|
||
|
||
/// 对象字段访问:`data["title"]?.asString`
|
||
nonisolated public subscript(key: String) -> BridgeData? {
|
||
if case .object(let o) = self { return o[key] }
|
||
return nil
|
||
}
|
||
|
||
/// 数组索引访问:`data[0]?.asString`
|
||
nonisolated public subscript(index: Int) -> BridgeData? {
|
||
if case .array(let a) = self, a.indices.contains(index) { return a[index] }
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// MARK: - 字面量构造
|
||
|
||
extension BridgeData: ExpressibleByStringLiteral {
|
||
public init(stringLiteral value: String) { self = .string(value) }
|
||
}
|
||
|
||
extension BridgeData: ExpressibleByIntegerLiteral {
|
||
public init(integerLiteral value: Int) { self = .number(Double(value)) }
|
||
}
|
||
|
||
extension BridgeData: ExpressibleByFloatLiteral {
|
||
public init(floatLiteral value: Double) { self = .number(value) }
|
||
}
|
||
|
||
extension BridgeData: ExpressibleByBooleanLiteral {
|
||
public init(booleanLiteral value: Bool) { self = .bool(value) }
|
||
}
|
||
|
||
extension BridgeData: ExpressibleByArrayLiteral {
|
||
public init(arrayLiteral elements: BridgeData...) { self = .array(elements) }
|
||
}
|
||
|
||
extension BridgeData: ExpressibleByDictionaryLiteral {
|
||
public init(dictionaryLiteral elements: (String, BridgeData)...) {
|
||
self = .object(Dictionary(uniqueKeysWithValues: elements))
|
||
}
|
||
}
|