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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0a313afe8e
commit
d38ff0f8c4
@@ -347,13 +347,15 @@ Contract Design Plan(本文档)
|
||||
|
||||
##### 2.A H5→Native handler(无外部依赖)
|
||||
|
||||
- [ ] **2.1** `gameCopytext` / `gamepastetext`(剪贴板)
|
||||
- [ ] **2.2** `vibrator` / `repeatvibrator` / `canclevibrator`(已有,扩展)
|
||||
- [ ] **2.3** `startshake` / `stopshake` / `SwitchShake`(摇一摇开关 + 音效开关)
|
||||
- [ ] **2.4** `voicePlaying`(语音播放总开关)
|
||||
- [ ] **2.5** `getphoneInfo` → 触发反向 callback `getphoneinfo`(注意小写 i)
|
||||
- [ ] **2.6** `browser` → 系统 Safari 打开 URL
|
||||
- [ ] **2.7** `opensaoma` 空实现注册(契约 §3.1【22】要求)
|
||||
- [x] **2.1** `gameCopytext` / `gamepastetext`(剪贴板)— `Source/Bridge/Handlers/ClipboardHandler.swift`,UIPasteboard.general
|
||||
- [x] **2.2** `vibrator` / `repeatvibrator` / `canclevibrator`(VibratorHandler.swift 扩展 2 项)
|
||||
- [x] **2.3** `startshake` / `stopshake` / `SwitchShake`(ShakeHandler.swift,canShake / canVoice 状态 + WebContainer motionEnded 转发 + shakeEnd 反向)
|
||||
- [x] **2.4** `voicePlaying`(VoicePlayingHandler.swift,actor VoiceCenter.shared.isEnabled 开关位,Phase 3 mediaTypeAudio 读此)
|
||||
- [x] **2.5** `getphoneInfo` → 触发反向 callback `getphoneinfo`(DeviceInfoHandler.swift,注意大小写:大写 I 入、小写 i 出;DeviceInfoSnapshot 6 字段,IDFA 用 idfv 兜底因为 CLAUDE.md 不接 ATT)
|
||||
- [x] **2.6** `browser` → 系统 Safari 打开 URL(BrowserHandler.swift,UIApplication.open async)
|
||||
- [x] **2.7** `opensaoma` 空实现注册(OpenSaomaHandler.swift,契约 §3.1【22】要求)
|
||||
- [x] **Bonus** `startlocation` stub(StartLocationHandler.swift,让 H5 启动不报 "no handler";Phase 5 接 LocationKit 完整实现)
|
||||
- [x] **Bridge 重构** `BridgeData.asXxx` 全部加 `nonisolated`,让 handler 闭包(@Sendable async)可以自由访问数据值;契约不变
|
||||
|
||||
##### 2.B Native→H5 反向 callback
|
||||
|
||||
|
||||
@@ -98,47 +98,50 @@ extension BridgeData {
|
||||
}
|
||||
|
||||
// MARK: - 访问语法糖
|
||||
// 注:项目默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 让所有类型默认 main 隔离;
|
||||
// BridgeData 是纯值类型、需要在 handler 闭包(@Sendable)内自由访问,
|
||||
// 所有访问语法糖标 nonisolated 避免跨 actor 限制。
|
||||
|
||||
extension BridgeData {
|
||||
|
||||
public var asString: String? {
|
||||
nonisolated public var asString: String? {
|
||||
if case .string(let s) = self { return s }
|
||||
return nil
|
||||
}
|
||||
|
||||
public var asDouble: Double? {
|
||||
nonisolated public var asDouble: Double? {
|
||||
if case .number(let d) = self { return d }
|
||||
return nil
|
||||
}
|
||||
|
||||
public var asInt: Int? {
|
||||
nonisolated public var asInt: Int? {
|
||||
if case .number(let d) = self { return Int(d) }
|
||||
return nil
|
||||
}
|
||||
|
||||
public var asBool: Bool? {
|
||||
nonisolated public var asBool: Bool? {
|
||||
if case .bool(let b) = self { return b }
|
||||
return nil
|
||||
}
|
||||
|
||||
public var asArray: [BridgeData]? {
|
||||
nonisolated public var asArray: [BridgeData]? {
|
||||
if case .array(let a) = self { return a }
|
||||
return nil
|
||||
}
|
||||
|
||||
public var asObject: [String: BridgeData]? {
|
||||
nonisolated public var asObject: [String: BridgeData]? {
|
||||
if case .object(let o) = self { return o }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 对象字段访问:`data["title"]?.asString`
|
||||
public subscript(key: String) -> BridgeData? {
|
||||
nonisolated public subscript(key: String) -> BridgeData? {
|
||||
if case .object(let o) = self { return o[key] }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 数组索引访问:`data[0]?.asString`
|
||||
public subscript(index: Int) -> BridgeData? {
|
||||
nonisolated public subscript(index: Int) -> BridgeData? {
|
||||
if case .array(let a) = self, a.indices.contains(index) { return a[index] }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// BrowserHandler.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 → Native handler:browser — 用系统 Safari 打开 URL
|
||||
// 契约:docs/H5-Native-Contract.md §3.1 [16]
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
public enum BrowserHandler {
|
||||
|
||||
public static func register(on bridge: any BridgeProtocol) {
|
||||
// 【16】browser
|
||||
// 入参 data: string(URL,需 URL 编码后传给 UIApplication.openURL)
|
||||
// cb:"browser"
|
||||
bridge.register("browser") { data, callback in
|
||||
defer { callback?(.string("browser")) }
|
||||
guard let urlStr = data?.asString,
|
||||
let url = URL(string: urlStr) ?? URL(string: urlStr.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
else {
|
||||
return
|
||||
}
|
||||
// UIApplication.open 在 iOS 15+ 是 async;用 Task 异步打开但不
|
||||
// await 返回值(H5 已经 cb 完成,不需要等 open 完成)
|
||||
Task { @MainActor in
|
||||
_ = await UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// ClipboardHandler.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 → Native handler:gameCopytext / gamepastetext
|
||||
// 契约:docs/H5-Native-Contract.md §3.1 [13][14]
|
||||
// 原项目:daoqi/msext NewRootVC.m(散落实现,UIPasteboard 同款)
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
public enum ClipboardHandler {
|
||||
|
||||
public static func register(on bridge: any BridgeProtocol) {
|
||||
// 【13】gamepastetext — 读剪贴板
|
||||
// 入参忽略;cb:当前剪贴板文本(不是 "gamepastetext",nil 时回空串)
|
||||
// 注:iOS 14+ 读取会触发系统"已粘贴自 XX"提示,是平台行为,不算契约违反
|
||||
bridge.register("gamepastetext") { _, callback in
|
||||
let text = UIPasteboard.general.string ?? ""
|
||||
callback?(.string(text))
|
||||
}
|
||||
|
||||
// 【14】gameCopytext — 写剪贴板
|
||||
// 入参 data: string;cb:"gameCopytext"
|
||||
bridge.register("gameCopytext") { data, callback in
|
||||
if let text = data?.asString {
|
||||
UIPasteboard.general.string = text
|
||||
}
|
||||
callback?(.string("gameCopytext"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// 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)
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// OpenSaomaHandler.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 → Native handler:opensaoma — 二维码扫描(**契约要求注册的空实现**)
|
||||
// 契约:docs/H5-Native-Contract.md §3.1 [22]
|
||||
//
|
||||
// 原 msext 也是空实现,H5 调了不会出错也不会有动作。新外壳保留空实现
|
||||
// 以保持契约(不注册的话 H5 会触发 "no handler registered" 错误分支)。
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum OpenSaomaHandler {
|
||||
|
||||
public static func register(on bridge: any BridgeProtocol) {
|
||||
// 【22】opensaoma — 空实现
|
||||
// 入参忽略;副作用:无;cb:"opensaoma"
|
||||
bridge.register("opensaoma") { _, callback in
|
||||
callback?(.string("opensaoma"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// ShakeHandler.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 → Native handler 三件套 + 反向 callback:
|
||||
// startshake / stopshake / SwitchShake → shakeEnd
|
||||
//
|
||||
// 契约:docs/H5-Native-Contract.md §3.1 [7][8][9] + §3.2 [5]
|
||||
// Design:§8.7 ShakeKit
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 摇一摇状态控制 + 反向 callback 桥接器(@MainActor 实例化,VC 注入)。
|
||||
///
|
||||
/// 由 WebContainerViewController 在 viewDidLoad 时创建,并:
|
||||
/// - 调用 `register(on:)` 注册 3 个 handler
|
||||
/// - 重写 `motionEnded(_:with:)` 转发到 `handleMotionEnded(_:)`
|
||||
/// - `viewDidAppear` 时调 `becomeFirstResponder()`(否则 motionEnded 不触发)
|
||||
@MainActor
|
||||
public final class ShakeHandler {
|
||||
|
||||
/// 摇一摇总开关(startshake / stopshake 控制)
|
||||
public private(set) var canShake: Bool = false
|
||||
|
||||
/// 摇一摇音效开关(SwitchShake 控制)
|
||||
public private(set) var canVoice: Bool = true
|
||||
|
||||
private weak var bridge: (any BridgeProtocol)?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func register(on bridge: any BridgeProtocol) {
|
||||
self.bridge = bridge
|
||||
|
||||
// 【7】startshake — 开摇
|
||||
// 入参忽略 → canshake = YES
|
||||
// cb:"startshake from accreditlogin"(字面字符串带后缀,沿用历史;
|
||||
// H5 不依赖此值,但严格拷贝保留契约等价)
|
||||
bridge.register("startshake") { [weak self] _, callback in
|
||||
Task { @MainActor in self?.canShake = true }
|
||||
callback?(.string("startshake from accreditlogin"))
|
||||
}
|
||||
|
||||
// 【8】stopshake — 关摇
|
||||
// 入参忽略 → canshake = NO;cb 同 startshake
|
||||
bridge.register("stopshake") { [weak self] _, callback in
|
||||
Task { @MainActor in self?.canShake = false }
|
||||
callback?(.string("startshake from accreditlogin"))
|
||||
}
|
||||
|
||||
// 【9】SwitchShake — 摇一摇音效开关
|
||||
// 入参 int 1 → canvoice = YES,其它 → canvoice = NO
|
||||
// cb:"SwitchShake"
|
||||
bridge.register("SwitchShake") { [weak self] data, callback in
|
||||
let on = (data?.asInt ?? 0) == 1
|
||||
Task { @MainActor in self?.canVoice = on }
|
||||
callback?(.string("SwitchShake"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 由 WebContainerViewController.motionEnded(_:with:) 转发调用。
|
||||
/// 触发反向 callback `shakeEnd`(仅 `canshake=YES` 时)。
|
||||
public func handleMotionEnded(_ motion: UIEvent.EventSubtype) {
|
||||
guard motion == .motionShake, canShake else { return }
|
||||
// Phase 2.3 暂不播放摇一摇音效(依赖 AudioKit / shake_sound_male.mp3,
|
||||
// 留待 Phase 3 AudioKit 完整时补;canVoice 状态保留供后续使用)
|
||||
bridge?.call("shakeEnd", data: nil, callback: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// StartLocationHandler.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 → Native handler:startlocation — 定位(Phase 5 完整实现,本期为 stub)
|
||||
// 契约:docs/H5-Native-Contract.md §3.1 [20] + §3.2 [6]
|
||||
//
|
||||
// Stub 行为:
|
||||
// - 接收 startlocation 调用,cb "startlocation"(响应不为 undefined,H5 不卡)
|
||||
// - **不触发** getlocationinfo 反向 callback(暂无高德 SDK,做不到真实定位)
|
||||
// - H5 业务侧若强依赖 getlocationinfo,需等 Phase 5 完整接入 AMapLocationKit
|
||||
//
|
||||
// 按 CLAUDE.md 原则 A 第一准则:H5 调啥就得有人接,即使没真实业务也要注册。
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum StartLocationHandler {
|
||||
|
||||
public static func register(on bridge: any BridgeProtocol) {
|
||||
bridge.register("startlocation") { _, callback in
|
||||
// Phase 2 stub:仅响应 callback,不真实定位
|
||||
// Phase 5 接入 LocationKit + 高德 AMapLocationKit.xcframework 时升级为完整实现
|
||||
callback?(.string("startlocation"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,31 +2,38 @@
|
||||
// VibratorHandler.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 → Native handler「vibrator」(契约 §3.1 [10]:单次振动)。
|
||||
// Phase 1.15 仅实现这一项作为最小垂直闭环验证标的;
|
||||
// `repeatvibrator` / `canclevibrator` 放到 Phase 2.2 一并补齐。
|
||||
// H5 → Native handler 三件套:vibrator / repeatvibrator / canclevibrator
|
||||
// 契约:docs/H5-Native-Contract.md §3.1 [10][11][12]
|
||||
// 原项目:daoqi/msext RootVC.m:1816-1834
|
||||
//
|
||||
|
||||
import AudioToolbox
|
||||
|
||||
/// 单次振动 handler 注册器。
|
||||
///
|
||||
/// 设计为无状态 enum:震动调用是无状态的 C API,不需要持有任何资源;
|
||||
/// 同时避免误把 handler 注册器实例化保留,造成"注册多份"。
|
||||
/// 振动 handler 注册器(无状态 enum)。
|
||||
public enum VibratorHandler {
|
||||
|
||||
/// 在 `bridge` 上注册 `vibrator` handler。
|
||||
///
|
||||
/// 契约(`docs/H5-Native-Contract.md` §3.1 [10]):
|
||||
/// - 入参:被忽略(参考原 msext `RootVC.m:1816-1818` 入参 `time` 实际不读取)
|
||||
/// - 副作用:`AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)`
|
||||
/// - responseCallback:字符串 `"vibrator"`
|
||||
///
|
||||
/// 模拟器无振动硬件,调用会被静默忽略(不抛异常),真机才能感知。
|
||||
/// 注册三个振动 handler。
|
||||
public static func register(on bridge: any BridgeProtocol) {
|
||||
// 【10】vibrator — 单次振动
|
||||
// 入参忽略;副作用:AudioServicesPlaySystemSound;cb "vibrator"
|
||||
bridge.register("vibrator") { _, callback in
|
||||
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
||||
callback?(.string("vibrator"))
|
||||
}
|
||||
|
||||
// 【11】repeatvibrator — 重复振动(msext RootVC.m:1820 实际就是单次 vibrate,
|
||||
// 不真的循环,沿用历史)
|
||||
bridge.register("repeatvibrator") { _, callback in
|
||||
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
||||
callback?(.string("repeatvibrator"))
|
||||
}
|
||||
|
||||
// 【12】canclevibrator — 释放系统振动音
|
||||
// AudioServicesDisposeSystemSoundID(kSystemSoundID_Vibrate) 对系统常量
|
||||
// 意义有限,但 H5 仍会调,必须 noop + cb 维持契约
|
||||
bridge.register("canclevibrator") { _, callback in
|
||||
AudioServicesDisposeSystemSoundID(kSystemSoundID_Vibrate)
|
||||
callback?(.string("canclevibrator"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// VoicePlayingHandler.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 → Native handler:voicePlaying — 远端语音播放总开关
|
||||
// 契约:docs/H5-Native-Contract.md §3.1 [6]
|
||||
//
|
||||
// Phase 2.4 仅实现"开关位",真正的 mediaTypeAudio 播放在 Phase 3 AudioKit
|
||||
// 完整时接入。开关位由 actor VoiceCenter 持有,跨 handler 共享。
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 远端语音播放总开关(actor 隔离,跨 bridge handler 安全共享)。
|
||||
/// Phase 3 mediaTypeAudio handler 读此 isEnabled 决定是否播放。
|
||||
public actor VoiceCenter {
|
||||
public static let shared = VoiceCenter()
|
||||
public private(set) var isEnabled: Bool = true
|
||||
public func setEnabled(_ on: Bool) { isEnabled = on }
|
||||
}
|
||||
|
||||
public enum VoicePlayingHandler {
|
||||
|
||||
public static func register(on bridge: any BridgeProtocol) {
|
||||
// 【6】voicePlaying
|
||||
// 入参 int 1=允许 / 其它=静默
|
||||
// cb:"Response from voicePlaying"
|
||||
bridge.register("voicePlaying") { data, callback in
|
||||
let on = (data?.asInt ?? 0) == 1
|
||||
Task { await VoiceCenter.shared.setEnabled(on) }
|
||||
callback?(.string("Response from voicePlaying"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ public final class WebContainerViewController: UIViewController {
|
||||
private let bridgedWebView = BridgedWebView()
|
||||
private let splash = SplashOverlay()
|
||||
|
||||
// MARK: - Handlers (有状态的 handler 注册器持有,无状态的走 enum 静态 register)
|
||||
|
||||
private let shakeHandler = ShakeHandler()
|
||||
|
||||
// MARK: - Boot pipeline 内部错误分类
|
||||
//
|
||||
// 用 enum 区分"终态阻塞"与"系统级错误":
|
||||
@@ -47,10 +51,37 @@ public final class WebContainerViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册所有 H5 → Native handler。Phase 1.15 仅 `vibrator`;
|
||||
/// 后续 Phase 2+ 在此追加更多 handler(保持单一聚合点便于发现 / 调试)。
|
||||
/// 注册所有 H5 → Native handler 的单一聚合点。
|
||||
/// Phase 1.15 仅 vibrator;Phase 2.A 扩展到 8 个 handler 模块。
|
||||
private func registerBridgeHandlers() {
|
||||
VibratorHandler.register(on: bridgedWebView.bridge)
|
||||
let bridge = bridgedWebView.bridge
|
||||
|
||||
// Phase 1.15
|
||||
VibratorHandler.register(on: bridge) // §3.1 [10][11][12]扩展
|
||||
|
||||
// Phase 2.A 大厅简单 handler(无外部依赖)
|
||||
ClipboardHandler.register(on: bridge) // §3.1 [13][14]
|
||||
shakeHandler.register(on: bridge) // §3.1 [7][8][9] + §3.2 [5]
|
||||
VoicePlayingHandler.register(on: bridge) // §3.1 [6]
|
||||
DeviceInfoHandler.register(on: bridge) // §3.1 [21] + §3.2 [1]
|
||||
BrowserHandler.register(on: bridge) // §3.1 [16]
|
||||
OpenSaomaHandler.register(on: bridge) // §3.1 [22]空 stub
|
||||
StartLocationHandler.register(on: bridge) // §3.1 [20]Phase 5 完整实现,Phase 2 stub
|
||||
}
|
||||
|
||||
// MARK: - 摇一摇支持
|
||||
// motionEnded 必须 first responder 才触发;canBecomeFirstResponder = true +
|
||||
// viewDidAppear becomeFirstResponder。详见 Design §8.7.1。
|
||||
|
||||
public override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
||||
public override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
|
||||
shakeHandler.handleMotionEnded(motion)
|
||||
}
|
||||
|
||||
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .landscape }
|
||||
|
||||
Reference in New Issue
Block a user