Design §8.7:新增 ShakeKit + 4 项接口实现骨架

覆盖 §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 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 07:31:08 +08:00
co-authored by Claude Opus 4.7
parent d48ecc0473
commit a06a18c3aa
+135
View File
@@ -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. 并发模型