Phase 1.16:SceneDelegate 接 WebContainer + 退役 RootViewController

- SceneDelegate.scene(_:willConnectTo:options:) 中 rootViewController
  从 M0 占位 RootViewController() 改为 WebContainerViewController()
- 删除 ylgamehall/RootViewController.swift(M0 + Phase 1.2–1.13 烟雾测试
  整体退役);所有 print 验证记录在 git log,留着是死代码
- File System Synchronized Group 自动适应文件删除,无需改 pbxproj
- BuildProject 通过

至此 Phase 1.16 完成;启动链路:
  SceneDelegate → WebContainerViewController.viewDidLoad
    → setupBridgedWebView + setupSplash + registerBridgeHandlers
    → runBootPipeline (ensureReady → fetch → resolve → upgrade → loadFileURL)
    → didFinish 淡出 splash

Plan 进度已勾选(§5 Phase 1.16 + §8)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 04:23:52 +08:00
co-authored by Claude Opus 4.7
parent 6c943e2924
commit c95e80df9c
3 changed files with 6 additions and 206 deletions
+5 -3
View File
@@ -312,8 +312,10 @@ Contract Design Plan(本文档)
- 入参忽略(参考 msext `RootVC.m:1816-1818``time` 参数实际不读取) - 入参忽略(参考 msext `RootVC.m:1816-1818``time` 参数实际不读取)
- 副作用:`AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)`responseCallback`.string("vibrator")` - 副作用:`AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)`responseCallback`.string("vibrator")`
- `WebContainerViewController.viewDidLoad` 新增 `registerBridgeHandlers()` 单一聚合点,Phase 2+ 在此追加。BuildProject 通过 - `WebContainerViewController.viewDidLoad` 新增 `registerBridgeHandlers()` 单一聚合点,Phase 2+ 在此追加。BuildProject 通过
- [ ] **1.16** `SceneDelegate``WebContainerViewController` 作 rootViewController - [x] **1.16** `SceneDelegate``WebContainerViewController` 作 rootViewController
- 替换 M0 占位 `RootViewController` - 替换 M0 占位 `RootViewController` + 删除 `ylgamehall/RootViewController.swift`(M0 烟雾测试堆栈整体退役,所有 1.2–1.13 print 验证记录在 git log,留着是死代码)
- File System Synchronized Group 自动适应文件删除,无需改 pbxproj
- BuildProject 通过
- [ ] **1.17** 把渠道目录里临时填入的 demo 值 + 一个 demo H5 跑通 / 真机验证完整 1.10-1.16 链路 - [ ] **1.17** 把渠道目录里临时填入的 demo 值 + 一个 demo H5 跑通 / 真机验证完整 1.10-1.16 链路
#### 验收 #### 验收
@@ -808,7 +810,7 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
- [x] 1.14.d WebContainerViewControllerSplashOverlay + 16:9 letterbox + 启动流水线 + 三种 alert - [x] 1.14.d WebContainerViewControllerSplashOverlay + 16:9 letterbox + 启动流水线 + 三种 alert
- [x] 1.14.e WebView didFinish 后 0.3s 淡出 splash + removeFromSuperview - [x] 1.14.e WebView didFinish 后 0.3s 淡出 splash + removeFromSuperview
- [x] 1.15 VibratorHandler(单次振动,AudioServicesPlaySystemSound + responseCallback "vibrator" - [x] 1.15 VibratorHandler(单次振动,AudioServicesPlaySystemSound + responseCallback "vibrator"
- [ ] 1.16 SceneDelegate → WebContainerViewController - [x] 1.16 SceneDelegate → WebContainerViewController(删除 M0 占位 RootViewController.swift
- [ ] 1.17 Demo H5 联调 + 真机验证升级路径 - [ ] 1.17 Demo H5 联调 + 真机验证升级路径
### Phase 2 大厅简单 handler + 反向 callback ### Phase 2 大厅简单 handler + 反向 callback
-202
View File
@@ -1,202 +0,0 @@
//
// RootViewController.swift
// ylgamehall
//
// M0 Coordinator + LobbyViewController
//
import UIKit
final class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Phase 1.2 ChannelConfig.plist 11
let cfg = BundleConfig.shared
print("""
[BundleConfig] 渠道注入读取结果:
qiniuDomain = \(cfg.qiniuDomain)
gameId = \(cfg.gameId)
channel = \(cfg.channel)
gameDir = \(cfg.gameDir)
gameStart = \(cfg.gameStart)
gameConfig = \(cfg.gameConfig)
market = \(cfg.market)
agent = \(cfg.agent)
appVersion = \(cfg.appVersion)
other = "\(cfg.other)"
appleConfig = "\(cfg.appleConfig)"
""")
// Phase 1.3 SandboxPaths
print("""
[SandboxPaths] 路径计算结果:
caches = \(SandboxPaths.caches.path)
documents = \(SandboxPaths.documents.path)
bundle = \(SandboxPaths.bundle.path)
lobbyRoot = \(SandboxPaths.lobbyRoot.path)
lobbyIndex = \(SandboxPaths.lobbyIndex.path)
lobbyVersionXML = \(SandboxPaths.lobbyVersionXML.path)
""")
// Phase 1.5 ResourceUnzipper lobby index.html
Task {
let start = Date()
do {
try await ResourceUnzipper.shared.ensureReady()
let elapsed = Date().timeIntervalSince(start)
let exists = FileManager.default.fileExists(atPath: SandboxPaths.lobbyIndex.path)
print("""
[ResourceUnzipper] ensureReady 完成(耗时 \(String(format: "%.3f", elapsed))s
lobbyIndex 存在: \(exists)
""")
} catch {
print("[ResourceUnzipper] ERROR: \(error)")
}
}
// Phase 1.8 WebViewJavascriptBridge.js Bundle
// synchronized group ylgamehall/Resources/JS/ bundle
// subdirectory
if let url = Bundle.main.url(forResource: "WebViewJavascriptBridge",
withExtension: "js"),
let content = try? String(contentsOf: url, encoding: .utf8) {
print("""
[WVJB.js] 已就绪
path = \(url.path)
bytes = \(content.utf8.count)
head = \(content.prefix(60).replacingOccurrences(of: "\n", with: ""))
""")
} else {
print("[WVJB.js] ERROR: 找不到 Bundle 内 JS/WebViewJavascriptBridge.js")
}
// Phase 1.10 .txt cache-busting +
Task {
let start = Date()
do {
let outcome = try await RemoteConfigClient.shared.fetch()
let elapsed = Date().timeIntervalSince(start)
switch outcome {
case .parsed(let cfg):
print("""
[RemoteConfigClient] 拉取成功 .parsed(耗时 \(String(format: "%.3f", elapsed))s
showmessage = \(cfg.showmessage ?? "<nil>")
agentlist count = \(cfg.agentlist?.count ?? 0)
""")
if let first = cfg.agentlist?.first {
print("""
agent[0].agentid = \(first.agentid ?? "<nil>")
agent[0].appVersion = \(first.appVersion ?? "<nil>")
agent[0].gameVersion = \(first.gameVersion ?? "<nil>")
agent[0].channellist = \(first.channellist?.count ?? 0)
agent[0].gamelist = \(first.gamelist?.count ?? 0)
""")
}
// Phase 1.11 chulishengji
let bc = BundleConfig.shared
let resolved = VersionResolver.resolve(
config: cfg,
agentId: bc.agent,
channelId: bc.channel,
marketId: bc.market,
gameId: bc.gameId
)
print("""
[VersionResolver] chulishengji 双子树合并结果:
appVersion = \(resolved.appVersion)
appDownload = \(resolved.appDownload ?? "<nil>")
gameVersion = \(resolved.gameVersion)
gameZip = \(resolved.gameZip ?? "<nil>")
showmessage = "\(resolved.showmessage ?? "<nil>")"
""")
// Phase 1.12 +
let localApp = LocalVersionReader.localAppVersion
let localGame = LocalVersionReader.localGameVersion
let needIPAUpgrade = resolved.appVersion > localApp
let needZipUpgrade = resolved.gameVersion > localGame
print("""
[LocalVersionReader] 本地 vs 远端对比:
本地 appVersion = \(localApp)(来自 ChannelConfig.plist
远端 appVersion = \(resolved.appVersion)
→ IPA 升级 = \(needIPAUpgrade ? "需要" : "不需要")
本地 gameVersion = \(localGame)(来自 version.xml /game/version@value
远端 gameVersion = \(resolved.gameVersion)
→ H5 zip 升级 = \(needZipUpgrade ? "需要" : "不需要")
""")
// Phase 1.13 / 1.14.c LobbyZipUpgrader
// progress 10%
let upgradeStart = Date()
do {
let lastReported = ProgressTicker()
let outcome = try await LobbyZipUpgrader.shared.upgradeIfNeeded(
resolved: resolved,
onProgress: { p in
lastReported.tickIfPassed(p) { reported in
print(String(format: "[LobbyZipUpgrader] 下载进度 %3d%%", Int(reported * 100)))
}
}
)
let elapsed = Date().timeIntervalSince(upgradeStart)
switch outcome {
case .noop:
print("""
[LobbyZipUpgrader] 无需升级,跳过(耗时 \(String(format: "%.3f", elapsed))s
""")
case .upgraded(let from, let to):
let newLocalGame = LocalVersionReader.localGameVersion
print("""
[LobbyZipUpgrader] 升级完成 \(from)\(to)(耗时 \(String(format: "%.3f", elapsed))s
重新读 version.xml: localGameVersion = \(newLocalGame)
lobbyIndex 存在: \(FileManager.default.fileExists(atPath: SandboxPaths.lobbyIndex.path))
""")
}
} catch {
print("[LobbyZipUpgrader] ERROR: \(error)")
}
case .shortText(let msg):
print("""
[RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s
运营杀手锏:应当弹窗显示并停止重试
内容 = "\(msg)"
""")
}
} catch {
print("[RemoteConfigClient] ERROR: \(error)")
}
}
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
.landscape
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
.landscapeRight
}
override var prefersStatusBarHidden: Bool { false }
}
/// Phase 1.14.c 使 10% progress
/// `@unchecked Sendable` Int NSLock
private final class ProgressTicker: @unchecked Sendable {
private let lock = NSLock()
private var lastBucket: Int = -1
func tickIfPassed(_ progress: Double, action: (Double) -> Void) {
let bucket = Int(progress * 10)
lock.lock()
let shouldFire = bucket > lastBucket
if shouldFire { lastBucket = bucket }
lock.unlock()
if shouldFire {
action(Double(bucket) / 10.0)
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
options connectionOptions: UIScene.ConnectionOptions) { options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return } guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene) let window = UIWindow(windowScene: windowScene)
window.rootViewController = RootViewController() window.rootViewController = WebContainerViewController()
self.window = window self.window = window
window.makeKeyAndVisible() window.makeKeyAndVisible()
} }