From a06a18c3aa1bf96364a31f5da0b3e4e7a5811e31 Mon Sep 17 00:00:00 2001 From: joywayer Date: Mon, 22 Jun 2026 07:31:08 +0800 Subject: [PATCH] =?UTF-8?q?Design=20=C2=A78.7=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20ShakeKit=20+=204=20=E9=A1=B9=E6=8E=A5=E5=8F=A3=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖 §3.1 [7]startshake / [8]stopshake / [9]SwitchShake + §3.2 [5]shakeEnd 反向 callback 共 4 项接口。 §8.7.1 ShakeKit module 骨架: - canShake / canVoice 开关位(@MainActor 实例属性,取代 msext 全局变量) - handleMotionEnded(.motionShake) 集中处理:判 canShake → 播音效 → 触发 callback - shake_sound_male.mp3 复用 AudioKit.AudioPlayer.playOnce 统一路径 §8.7.1 接入 VC: - WebContainerViewController.canBecomeFirstResponder = true - viewDidAppear 调 becomeFirstResponder(必要,否则 motionEnded 不触发) - motionEnded(_:with:) 转发到 shakeKit.handleMotionEnded §8.7.2 H5 handler 实现: - 【7】startshake : canshake=YES + cb "startshake from accreditlogin" (字面字符串带 "from accreditlogin" 后缀沿用历史) - 【8】stopshake : canshake=NO + cb 同上 - 【9】SwitchShake : data int → canvoice = (1==YES) + cb "SwitchShake" - 反向 [5]shakeEnd: data=nil,由 motionEnded → onShakeEnded 触发 §8.7.3 与 msext 6 维度差异表(全局变量 → 实例属性 / 音效统一 / motionEnded 集中处理 / canBecomeFirstResponder 基类统一 / responseCallback 字面保留) Co-Authored-By: Claude Opus 4.7 --- docs/H5-Native-Implementation-Design.md | 135 ++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/docs/H5-Native-Implementation-Design.md b/docs/H5-Native-Implementation-Design.md index e06d47f..c795585 100644 --- a/docs/H5-Native-Implementation-Design.md +++ b/docs/H5-Native-Implementation-Design.md @@ -2839,6 +2839,141 @@ extension LocationInfo { | latitude 字段类型 | `stringWithFormat:@"%f"` 字符串 | `String(format: "%f", ...)`(等价) | | province 大小写 | 小写 p(与 sharelogin 大写 P 不一致)| 严格保持 | +### 8.7 ShakeKit + +覆盖契约 §C §3.1 [7][8] [9] 摇一摇 3 项 + §3.2 [5]`shakeEnd` 反向 callback,共 4 项接口。 + +设备摇动的真正事件源是 `UIResponder.motionEnded(_:with:)`(系统检测晃动后回调),ShakeKit 把它包装为:开关位(canshake / canvoice)+ 摇一摇音效播放 + 触发 shakeEnd callback。 + +#### 8.7.1 Module 骨架 + +```swift +// Source/ShakeKit/ShakeKit.swift +@MainActor +public final class ShakeKit { + + /// 摇一摇总开关(startshake / stopshake 控制) + public private(set) var canShake: Bool = false + /// 摇一摇音效开关(SwitchShake 控制) + public private(set) var canVoice: Bool = true + + public var onShakeEnded: (() -> Void)? + + private let audioPlayer: AudioPlayer + private let shakeSoundURL: URL // 拷贝自 docs/res/Res/shake_sound_male.mp3 → Bundle + + public init(audioPlayer: AudioPlayer, shakeSoundURL: URL) { + self.audioPlayer = audioPlayer + self.shakeSoundURL = shakeSoundURL + } + + public func setShakeEnabled(_ on: Bool) { canShake = on } + public func setVoiceEnabled(_ on: Bool) { canVoice = on } + + /// 由 WebContainerViewController.motionEnded(_:with:) 转发调用 + public func handleMotionEnded(_ motion: UIEvent.EventSubtype) { + guard motion == .motionShake, canShake else { return } + if canVoice { audioPlayer.playOnce(shakeSoundURL) } + onShakeEnded?() + } +} +``` + +**接入 VC 层**(在 §4.2.1「启动期容易遗漏的契约点」已声明 `UIApplication.shared.applicationSupportsShakeToEdit = true`,本节补 motionEnded 转发): + +```swift +extension WebContainerViewController { + public override var canBecomeFirstResponder: Bool { true } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // 必须主动 becomeFirstResponder,否则 motionEnded: 不会触发 + becomeFirstResponder() + } + + public override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + shakeKit.handleMotionEnded(motion) + } +} +``` + +#### 8.7.2 H5 shake handlers + 反向 callback 骨架 + +```swift +// Source/Bridge/Handlers/ShakeHandlers.swift +@MainActor +public struct ShakeHandlers { + + let bridge: BridgeProtocol + let shakeKit: ShakeKit + + public func register() { + bridge.register("startshake", handler: startShake) + bridge.register("stopshake", handler: stopShake) + bridge.register("SwitchShake", handler: switchShake) + bindReverseCallback() + } + + // MARK: - 【7】 startshake + // + // 契约 §3.1 [7]: + // 入参 : 忽略 → canshake = YES + // responseCallback: "startshake from accreditlogin" + // (字面字符串带 from accreditlogin 后缀,沿用历史; + // H5 不依赖此值,但严格拷贝保留契约等价) + + private func startShake(_ data: BridgeData?, _ cb: BridgeCallback?) async { + shakeKit.setShakeEnabled(true) + cb?(.string("startshake from accreditlogin")) + } + + // MARK: - 【8】 stopshake + // + // 契约 §3.1 [8]: + // 入参 : 忽略 → canshake = NO + // responseCallback: 同 startshake("startshake from accreditlogin") + + private func stopShake(_ data: BridgeData?, _ cb: BridgeCallback?) async { + shakeKit.setShakeEnabled(false) + cb?(.string("startshake from accreditlogin")) + } + + // MARK: - 【9】 SwitchShake — 摇一摇音效开关 + // + // 契约 §3.1 [9]: + // 入参 : int 1 → canvoice = YES,其他 → canvoice = NO + // responseCallback: "SwitchShake" + + private func switchShake(_ data: BridgeData?, _ cb: BridgeCallback?) async { + defer { cb?(.string("SwitchShake")) } + let on = (data?.asInt ?? 0) == 1 + shakeKit.setVoiceEnabled(on) + } + + // MARK: - §3.2 [5] shakeEnd 反向 callback + // + // 触发时机:UIResponder.motionEnded(.motionShake, with:) 且 canshake == YES + // 数据:nil(契约规定 data 为 nil,不传额外字段) + + private func bindReverseCallback() { + shakeKit.onShakeEnded = { [weak bridge] in + bridge?.call("shakeEnd", data: nil, callback: nil) + } + } +} +``` + +#### 8.7.3 与原 msext 的差异 + +| 维度 | msext 现状(不要照抄) | 新外壳决策 | +|------|--------------------|----------| +| 开关状态 | `static BOOL canshake` / `static BOOL canvoice` 全局变量 | `@MainActor ShakeKit` 实例属性,VC 注入 | +| 摇一摇音效 | `[FuncPublic playWithFileName:@"shake_sound_male.mp3"]` 散落 | 复用 AudioKit 的 `AudioPlayer.playOnce`,统一路径 | +| 摇动事件 | 大厅 / 子游戏 VC 各自实现 `motionEnded:` 各自判断 | ShakeKit 集中判断 + 单点 callback 触发 | +| canBecomeFirstResponder | 大厅 VC override 为 YES,子游戏沿用默认 NO(潜在漏点) | 统一在 WebContainerViewController 基类内 override = true | +| responseCallback 字面 | `"startshake from accreditlogin"` 字面奇怪但沿用 | 严格保留(H5 不读,仅保契约不破) | +| shakeEnd 数据 | `nil` | 严格保留(Bridge.call data: nil) | + --- ## 9. 并发模型