diff --git a/ylgamehall/Source/Bridge/BridgeProtocol.swift b/ylgamehall/Source/Bridge/BridgeProtocol.swift new file mode 100644 index 0000000..0269955 --- /dev/null +++ b/ylgamehall/Source/Bridge/BridgeProtocol.swift @@ -0,0 +1,173 @@ +// +// 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: - 访问语法糖 + +extension BridgeData { + + public var asString: String? { + if case .string(let s) = self { return s } + return nil + } + + public var asDouble: Double? { + if case .number(let d) = self { return d } + return nil + } + + public var asInt: Int? { + if case .number(let d) = self { return Int(d) } + return nil + } + + public var asBool: Bool? { + if case .bool(let b) = self { return b } + return nil + } + + public var asArray: [BridgeData]? { + if case .array(let a) = self { return a } + return nil + } + + public var asObject: [String: BridgeData]? { + if case .object(let o) = self { return o } + return nil + } + + /// 对象字段访问:`data["title"]?.asString` + public subscript(key: String) -> BridgeData? { + if case .object(let o) = self { return o[key] } + return nil + } + + /// 数组索引访问:`data[0]?.asString` + 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)) + } +}