启动文字提示,h5调试加入

This commit is contained in:
joywayer
2026-06-24 21:20:05 +08:00
parent 1d231a52ef
commit 06b7686e17
12 changed files with 70 additions and 29 deletions
+1 -1
View File
@@ -231,7 +231,7 @@ H5 → Native 时 data 字典中的 key,以及 Native → H5 callback payload
- `sharelogin` 回包中 `Province` 是**大写 P**(不是 `province`
- `OpenurlTitleData` 入参中 `"title "` 末尾**有一个空格**(不是 `"title"`
- `getphoneinfo` 回包用**小写 i**(H5 调用方是 `getphoneInfo` 大写 I
- `appservice` 用字符串 `"1"`(后台)/ `"2"`(前台),不是数字
- `appservice` 用字符串 `"2"`(后台)/ `"1"`(前台),不是数字 ⚠️ 值错位沿用 msext WKWebView 路径历史,参 `docs/H5-Native-Contract.md` §3.2 `appservice` 值错位说明
- `getlocationinfo``latitude` / `longitude`**string** 而不是 number`stringWithFormat:@"%f"` 产生)
### 3. 参数数据结构
+1 -1
View File
@@ -367,7 +367,7 @@ Contract Design Plan(本文档)
- [x] **2.10** `Source/Resource/NetworkMonitor.swift`NWPathMonitor 包装(Phase 1 已建)
- 状态变化 → 重写 `app_network.js` + `bridge.call("getnetwork", data: .string("1"/"2"/"3"))`
- [x] **2.11** `Source/Resource/AppLifecycleObserver.swift`:监听 `UIApplication.didEnterBackgroundNotification` / `willEnterForegroundNotification`
- 后台 → `bridge.call("appservice", data: .string("1"))`;前台 → `"2"`
- 后台 → `bridge.call("appservice", data: .string("2"))`;前台 → `"1"`(值错位沿用 msext WKWebView 路径历史,参 Contract.md §3.2 `appservice` 值错位说明)
- [x] **2.12** ShakeDetector 集成在 `Source/Bridge/Handlers/ShakeHandler.swift`Phase 2.3 一并完成)
- `WebContainerViewController.motionEnded(_:with:)` 转发 → `bridge.call("shakeEnd", data: nil)`(仅 `canShake=YES` 时触发)
+18 -8
View File
@@ -11,7 +11,7 @@
新外壳与现网外壳之间,**只有以下三类内容是契约,必须 1:1 一致**:
1. **桥接接口名** — H5 调用的 handler 名字符串(如 `mediaTypeAudio``friendsSharetypeUrlToptitleDescript``OpenurlTitleData`),少一个字符 H5 就找不到。
2. **参数字段名 & 数据结构** — H5 传入的 `data` 字段名、Native 回调时的 payload 结构。**含历史包袱**`sharelogin.Province`(大写 P)、`OpenurlTitleData` 的入参键名 `"title "`(末尾有空格)、`appservice``"1"`/`"2"` 字符串而不是数字、字段值约定(如 `isloop: -1/0/1` 三态),**必须照抄**。
2. **参数字段名 & 数据结构** — H5 传入的 `data` 字段名、Native 回调时的 payload 结构。**含历史包袱**`sharelogin.Province`(大写 P)、`OpenurlTitleData` 的入参键名 `"title "`(末尾有空格)、`appservice`字符串 `"2"`=进入后台 / `"1"`=回到前台(值错位,沿用 msext WKWebView 路径历史,参 §3.2 第 4 行)、字段值约定(如 `isloop: -1/0/1` 三态),**必须照抄**。
3. **空实现接口也必须注册**`opensaoma``getGameplay` 等当前为空实现的 handler,新外壳必须以同名空 handler 注册(响应 callback 但不做任何事),否则 H5 调用时会触发"bridge not found"错误分支。
**除以上三类之外,原生内部一切都可以重写**:技术栈(Swift/SwiftUI/Combine 都行)、WebView 接入方式(直接用 WKScriptMessageHandler 或继续用 WebViewJavascriptBridge)、网络层(URLSession/Alamofire)、解压库(ZIPFoundation)、状态机、控制器层级、命名风格——只要外部行为不变,怎么实现都行。本文档后续章节中描述的"现有实现细节"只是为了帮助你理解**为什么 H5 期待某种行为**,不是要求你照抄实现方式。
@@ -558,7 +558,7 @@ window.rootViewController = NavgationController
| 1 | `getphoneinfo` | 注册 `getphoneInfo` handler 收到调用后 | 见下方表 A | ✓ | ✓ |
| 2 | `getBattery` | UIDeviceBatteryLevelDidChangeNotification | 字符串 `"%.2f"` 形式的小数电量(0~1)| ✓ | ✓ |
| 3 | `getnetwork` | AFNetworkReachability 变化 | 字符串 `"1"`/`"2"`/`"3"` 1=无网,2=WiFi3=蜂窝)| ✓ | ✓ |
| 4 | `appservice` | App 前后台切换 | 字符串 `"1"`=进入后台,`"2"`=回到前台(命名错位,沿用历史| ✓ | ✓ |
| 4 | `appservice` | App 前后台切换 | 字符串 `"2"`=进入后台,`"1"`=回到前台(错位,沿用 msext WKWebView 路径历史。详见下方注释| ✓ | ✓ |
| 5 | `shakeEnd` | motionEnded 摇一摇结束 | `nil` | ✓ | ✓ |
| 6 | `getlocationinfo` | 定位成功/失败 | 见下方表 B | ✓ | ✓ |
| 7 | `gameui_play_voice` | mediaTypeAudio 开始播放(且 voicePlaying=1 时)| 字符串 user_id | ✓ | ✓ |
@@ -575,6 +575,16 @@ window.rootViewController = NavgationController
- `phonestate`H5 子游戏会在收到 `"2"` 时主动哑麦/暂停,收到 `"0"` 恢复,跟视频房间体验强相关
- `recordSuccess`H5 收到后会拿 `fileUrl` 而非 `audiourl` 走业务(早期接口与新接口并存的兼容产物);新外壳两个 handler 都得发,data 字段不同
**`appservice` 值错位说明**(极容易看反,新人务必照抄不要"修正"):
原 msext WKWebView 路径(`NewRootVC.m:1769-1786` / `gameController.m:1319-1336`)的真实代码是:
- iOS `applicationDidEnterBackground`**真·进入后台**)→ `AppDelegate` post 一个**名为 `enterForeground` 的 NSNotification**NSNotification 名错位,沿用历史)→ VC 的 `-enterForeground` 方法触发 → `[_bridge callHandler:@"appservice" data:@"2"]`
- iOS `applicationDidBecomeActive`**真·回到前台**)→ `AppDelegate` post 一个**名为 `applicationWillResignActive` 的 NSNotification**NSNotification 名错位)→ VC 的 `-applicationWillResignActive` 方法触发 → `[_bridge callHandler:@"appservice" data:@"1"]`
H5 端 N 年来收到的实际语义就是:**`"2"` = 进入后台、`"1"` = 回到前台**。新外壳不论 NSNotification 内部命名怎么改、用 `willEnterForegroundNotification` 还是 `didBecomeActiveNotification`,桥发出的字符串值必须按上面这套对应关系发,**不要被 NSNotification 名误导而把值发反**。
(顺带:原 msext 还有一条 iOS<9 UIWebView 路径 `RootVC.m:516-543` / `fourviewVC.m:1254-1282`,发的值反过来:进入后台 → `"1"` / 回到前台 → `"2"`。该路径在新外壳最低 iOS 15.6 下完全不可达,**不能作为对齐依据**,请忽略。)
**表 A — `getphoneinfo`**
```json
{
@@ -880,18 +890,18 @@ zip 包下载 + 解压由 `ASIHTTPRequest`(旧)/ `AFNetworking`(新)+ `Z
| 通知名 | 发送方 | 接收方 | 用途 |
|--------|--------|--------|------|
| `enterForeground` | AppDelegate `applicationDidEnterBackground` | Root/NewRootVC/fourview/game/three | 转 `appservice("2")` |
| `applicationWillResignActive` | AppDelegate `applicationDidBecomeActive`(节流 0.3s | 同上 | 转 `appservice("1")` |
| `enterForeground` | AppDelegate `applicationDidEnterBackground`(真·进入后台)| Root/NewRootVC/fourview/game/three | 转 `appservice("2")`(即 H5 收到 "2" 才是真·进入后台)|
| `applicationWillResignActive` | AppDelegate `applicationDidBecomeActive`真·回到前台,节流 0.3s | 同上 | 转 `appservice("1")`(即 H5 收到 "1" 才是真·回到前台)|
| `backgameDatatwo` | threeView / 子游戏 / RootVC.backgameData: | 父大厅 | 转 `getWebdata(data)` |
| `stopradio` | 任意 | 主 VC | 暂停 backgroundPlayer |
| `canbackye` | 任意(防抖恢复)| 主 VC | 允许返回手势 |
| `UIDeviceBatteryLevelDidChangeNotification` | 系统 | 主 VC | 转 `getBattery(level)` |
**命名注意**:旧 msext 中 `enterForeground` 实际表示"进入后台"`applicationWillResignActive` 实际表示"回到前台"(命名被换反,历史包袱)。
**命名注意**:旧 msext 中 `enterForeground` 实际表示"进入后台"`applicationWillResignActive` 实际表示"回到前台"(命名被换反,历史包袱)。**这个命名错位顺势把桥的字符串值也带反了**——H5 收到 `appservice("2")` 才表示进入后台、收到 `appservice("1")` 才表示回到前台。
这些 NSNotification 是**模块内通讯****不是 H5 可见的契约边界** —— H5 只能看到桥事件 `appservice("1")`/`appservice("2")`
这些 NSNotification 是**模块内通讯****不是 H5 可见的契约边界** —— H5 只能看到桥事件 `appservice("2")`(后台)/`appservice("1")`(前台)
新项目自由命名(推荐用 `.appDidEnterBackground` / `.appDidBecomeActive` 等清晰命名),只要最终把 `"1"`/`"2"` 正确转发给 H5 桥即可。详见 `H5-Native-Implementation-Design.md` §4.4。
新项目自由命名(推荐用 `.appDidEnterBackground` / `.appDidBecomeActive` 等清晰命名),但桥发出的值**必须按上面错位关系**对应:`didEnterBackground → "2"` / `willEnterForeground → "1"`**不要按 NSNotification 字面语义改回去**。详见 `H5-Native-Implementation-Design.md` §4.4 与 §3.2 `appservice` 值错位说明
---
@@ -1049,7 +1059,7 @@ Library/Caches/
- [ ] **H5 调 `confirm("是否退出")`** → 弹"取消"/"确定"两按钮 → 取消返回 false / 确定返回 true,H5 业务正常分支
### C. 系统事件
- [ ] 切后台 → H5 收到 `appservice("1")`;回前台 → 收到 `appservice("2")`
- [ ] 切后台 → H5 收到 `appservice("2")`;回前台 → 收到 `appservice("1")` ⚠️ 值错位沿用历史,参 §3.2 `appservice` 值错位说明
- [ ] 电量变化 → H5 收到 `getBattery("0.XX")`
- [ ] 飞行模式开关 → H5 收到 `getnetwork("1")` / `getnetwork("2"|"3")`
- [ ] 摇一摇(startshake==YES 时)→ H5 收到 `shakeEnd`
+17 -11
View File
@@ -267,7 +267,8 @@ final class ExternalSubscriptions {
bridge?.call("getnetwork", data: .string(state.rawValue), callback: nil)
}
lifecycle.onChange = { [weak bridge] state in
bridge?.call("appservice", data: .string(state == .background ? "1" : "2"), callback: nil)
// 值错位沿用历史 msext WKWebView 路径:背景="2" / 前台="1"
bridge?.call("appservice", data: .string(state == .background ? "2" : "1"), callback: nil)
}
battery.start(); network.start(); lifecycle.start()
// 立即派发一次,补齐暂停期间的状态
@@ -1069,18 +1070,20 @@ func sceneDidBecomeActive(_ scene: UIScene) {
}
// LobbyHandlers / SubGameHandlers 内监听并转桥
// 注:契约值与 NSNotification 字面语义相反,沿用 msext WKWebView 路径历史。
// 真·进入后台 → "2";真·回到前台 → "1"。详见 Contract.md §3.2 `appservice` 值错位说明。
notifications.observe(.appDidEnterBackground) { [bridge] _ in
bridge.call("appservice", data: .string("1"), callback: nil) // 契约:"1"=后台
bridge.call("appservice", data: .string("2"), callback: nil) // 契约:进入后台="2"
}
notifications.observe(.appDidBecomeActive) { [bridge] _ in
bridge.call("appservice", data: .string("2"), callback: nil) // 契约:"2"=前台
bridge.call("appservice", data: .string("1"), callback: nil) // 契约:回到前台="1"
}
```
**0.3s 节流决策**:旧外壳在 `applicationDidBecomeActive` 里设了 `flag=NO` + 0.3s 后恢复,防止双重触发。新外壳**默认不要这个节流**,理由:
1. 节流的根因是旧代码在 `applicationDidBecomeActive` 内被多处触发,新外壳只有 SceneDelegate 一处发通知
2. 测试如果证实双触发,加节流是 1 行代码的事(`DispatchQueue.asyncAfter`),不是架构改动
3. H5 端通常对重复 `appservice("2")` 是幂等的(就是刷新 UI 状态)
3. H5 端通常对重复 `appservice("1")`(回前台)是幂等的(就是刷新 UI 状态)
如真机回归发现 H5 双触发产生问题,在 LobbyHandlers 内加 throttle 即可,不污染 SceneDelegate。
@@ -1253,7 +1256,8 @@ extension BridgeProtocol {
}
func callAppService(_ state: AppState) {
call("appservice", data: .string(state == .background ? "1" : "2"), callback: nil)
// 值错位沿用历史 msext WKWebView 路径:背景="2" / 前台="1"
call("appservice", data: .string(state == .background ? "2" : "1"), callback: nil)
}
// ... 15 个全列出
}
@@ -2231,11 +2235,12 @@ private func runBootPipelineSteps() async throws {
bridge.call("getnetwork", data: .string("\(code)"), callback: nil)
}
// appservice 无对应 app_* 全局变量,只走 bridge.call
// 值错位沿用历史 msext WKWebView 路径:背景="2" / 前台="1"
AppLifecycleObserver.shared.onBackground = {
bridge.call("appservice", data: .string("1"), callback: nil)
bridge.call("appservice", data: .string("2"), callback: nil)
}
AppLifecycleObserver.shared.onForeground = {
bridge.call("appservice", data: .string("2"), callback: nil)
bridge.call("appservice", data: .string("1"), callback: nil)
}
}
```
@@ -2877,12 +2882,13 @@ public struct DeviceHandlers {
networkMonitor.start()
// 【4】 appservice — App 前后台切换
// 数据:字符串 "1"=进入后台 / "2"=回到前台(命名错位沿用历史
// 数据:字符串 "2"=进入后台 / "1"=回到前台(错位沿用 msext WKWebView 路径历史
// 参 Contract.md §3.2 `appservice` 值错位说明,以及原 NewRootVC.m:1773/1782
appLifecycle.onBackground = { [weak bridge] in
bridge?.call("appservice", data: .string("1"), callback: nil)
bridge?.call("appservice", data: .string("2"), callback: nil)
}
appLifecycle.onForeground = { [weak bridge] in
bridge?.call("appservice", data: .string("2"), callback: nil)
bridge?.call("appservice", data: .string("1"), callback: nil)
}
appLifecycle.start()
}
@@ -4263,7 +4269,7 @@ extension BridgeProtocol {
func callGetPhoneInfo(_ info: [String: String]) { call("getphoneinfo", data: .object(info.mapValues(BridgeData.string)), callback: nil) }
func callGetBattery(_ level: Float) { call("getBattery", data: .string(String(format: "%.2f", level)), callback: nil) }
func callGetNetwork(_ state: NetworkState) { call("getnetwork", data: .string(state.rawValue), callback: nil) } // "1"/"2"/"3"
func callAppService(_ s: AppLifecycleState) { call("appservice", data: .string(s == .background ? "1" : "2"), callback: nil) }
func callAppService(_ s: AppLifecycleState) { call("appservice", data: .string(s == .background ? "2" : "1"), callback: nil) } // 值错位沿用 msext WKWebView 路径
func callShakeEnd() { call("shakeEnd", data: nil, callback: nil) }
func callGetLocationInfo(_ d: LocationPayload) { call("getlocationinfo", data: .object(d.dict), callback: nil) } // 字段见契约 §3.2 表B
func callGameUIPlayVoice(_ user: String) { call("gameui_play_voice", data: .string(user), callback: nil) }
+1 -1
View File
@@ -152,7 +152,7 @@ H5 需先用 `bridge.registerHandler('callbackName', fn)` 注册:
- [ ] **F.1 模拟器 Features → Battery State 切换** → H5 收到 `getBattery` "0.XX" + `window.app_getbattery` 实时跟随
- [ ] **F.2 模拟器 Features → Network Link Conditioner 切换 100% Loss** → H5 收到 `getnetwork` "1" + `window.app_getnetwork === 1`;切回 WiFi → 收 "2";切 3G → 收 "3"
- [ ] **F.3 App 切后台**(Home 键 / 上滑)→ H5 收到 `appservice` "1";回前台 → 收 "2"
- [ ] **F.3 App 切后台**(Home 键 / 上滑)→ H5 收到 `appservice` "2";回前台 → 收 "1" ⚠️ 值错位沿用 msext WKWebView 路径历史,参 Contract.md §3.2
- [ ] **F.4 摇一摇** → shakeEnd(见 E.7
- [ ] **F.5 getphoneInfo 触发 getphoneinfo**(见 E.10
+2
View File
@@ -222,6 +222,7 @@
"$(PROJECT_DIR)/ylgamehall/Source/Audio/AMRFileCodec",
);
INFOPLIST_FILE = ylgamehall/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "进贤聚友棋牌";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = "";
@@ -271,6 +272,7 @@
"$(PROJECT_DIR)/ylgamehall/Source/Audio/AMRFileCodec",
);
INFOPLIST_FILE = ylgamehall/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "进贤聚友棋牌";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = "";
@@ -5,7 +5,8 @@
// UIApplication / onBackground / onForeground
//
// docs/H5-Native-Contract.md §3.2 4appservice callback
// "1" = / "2" = 沿
// "2" = / "1" = 沿 msext WKWebView
// daoqi/msext NewRootVC.m:1773/1782gameController.m:1323/1332
// Phase 2.11
//
@@ -53,6 +53,11 @@ public final class BridgedWebView: UIView {
// WKWebView + BridgeBus
let webView = WKWebView(frame: .zero, configuration: configuration)
#if DEBUG
if #available(iOS 16.4, *) {
webView.isInspectable = true
}
#endif
let bridge = BridgeBus(webView: webView,
controller: configuration.userContentController)
@@ -108,6 +108,11 @@ public final class OverlayViewController: UIViewController {
}
webView = WKWebView(frame: .zero, configuration: config)
#if DEBUG
if #available(iOS 16.4, *) {
webView.isInspectable = true
}
#endif
webView.scrollView.bounces = false
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.translatesAutoresizingMaskIntoConstraints = false
@@ -43,7 +43,8 @@ final class SplashOverlay: UIView {
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
label.textColor = .white
// SplashImage
label.textColor = .black
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textAlignment = .center
label.numberOfLines = 1
@@ -336,11 +336,13 @@ public final class SubGameViewController: UIViewController {
bridge.call("getnetwork", data: .string("\(code)"), callback: nil)
}
// §3.2 4 appservice 沿 msext WKWebView
// · "2"· "1" WebContainerViewController
AppLifecycleObserver.shared.onBackground = {
bridge.call("appservice", data: .string("1"), callback: nil)
bridge.call("appservice", data: .string("2"), callback: nil)
}
AppLifecycleObserver.shared.onForeground = {
bridge.call("appservice", data: .string("2"), callback: nil)
bridge.call("appservice", data: .string("1"), callback: nil)
}
// §3.1 14 "2" / "0"
@@ -359,6 +359,12 @@ public final class WebContainerViewController: UIViewController {
throw BootError.ipaUpgradeRequired(downloadURL: dl)
}
// config zip ""
// "..." zip CDN DNS+TLS
// " 1%" UI
//
splash.update(text: "校验版本中...", progress: nil)
// H5 zip zip splash.showError
// LobbyZipUpgrader.onProgress URLSession delegate
// hop MainActor splash UI
@@ -450,12 +456,15 @@ public final class WebContainerViewController: UIViewController {
bridge.call("getnetwork", data: .string("\(code)"), callback: nil)
}
// §3.2 4 appservice App 沿
// §3.2 4 appservice App 沿 msext WKWebView
// · "2"· "1" daoqi/msext NewRootVC.m:1773/1782
// gameController.m:1323/1332AppDelegate post NSNotification
// H5 ""
AppLifecycleObserver.shared.onBackground = {
bridge.call("appservice", data: .string("1"), callback: nil)
bridge.call("appservice", data: .string("2"), callback: nil)
}
AppLifecycleObserver.shared.onForeground = {
bridge.call("appservice", data: .string("2"), callback: nil)
bridge.call("appservice", data: .string("1"), callback: nil)
}
// §3.2 12 getWebdata backgameData / data H5