Files
youle_app_ios_v2/ylgamehall/Source/Bridge/BridgeProtocol.swift
T
joywayerandClaude Opus 4.7 d38ff0f8c4 Phase 2.A:大厅 8 个简单 handler 落地 + BridgeData 加 nonisolated
闭合 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>
2026-06-22 18:38:00 +08:00

177 lines
6.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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))
}
}