// // OverlayViewController.swift // ylgamehall // // 弹层(msext threeView 等价):WKWebView 加载外部 HTTP 页面, // H5 通过 `window.settings.{browser,finishweb,backgameData}` 三接口与原生交互。 // // 契约:docs/H5-Native-Contract.md §3.4 弹层桥 // Design:§3.4.2 / §2.4.3 栈深 ≤ 3 // // 与 msext threeView.m 的差异: // - msext 用 UIWebView + JSExport (Bridgetwo);新外壳用 WKWebView + WKScriptMessageHandler, // 在 .atDocumentEnd 注入 `window.settings = {...}` polyfill 桥到 messageHandlers // - msext orientation=1 时 CGAffineTransformMakeRotation(±π/2) 旋转 webView; // 本项目 landscape-locked,orientation 字段保留但不主动旋转(如有 H5 反馈需要再补) // - msext 用全局 sharedHTTPCookieStorage;本项目用 WKWebsiteDataStore.nonPersistent() // 隔离弹层 Cookie / 缓存,避免污染主业务态(Design §3.4.2 风险条目) // import UIKit import WebKit public final class OverlayViewController: UIViewController { // MARK: - 入参 private let request: OverlayRequest // MARK: - UI private var webView: WKWebView! /// WKScriptMessageHandler 名字(与 H5 端 polyfill 内 `webkit.messageHandlers.` 一致)。 /// 弹层 WKWebView 与大厅 WVJB 在不同 channel,可以与 WVJB 同名 handler 共存不冲突。 /// fileprivate 让同文件下的 ScriptMessageProxy 可用 `OverlayViewController.MessageName(rawValue:)` fileprivate enum MessageName: String, CaseIterable { case browser case finishweb case backgameData } // MARK: - Init public init(request: OverlayRequest) { self.request = request super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("OverlayViewController requires OverlayRequest") } // MARK: - Lifecycle public override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black title = request.title setupWebView() loadRequest() } public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .landscape } public override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { .landscapeRight } public override var prefersStatusBarHidden: Bool { false } // MARK: - Setup private func setupWebView() { let config = WKWebViewConfiguration() // 与大厅 / 子游戏共用默认 websiteDataStore,cookie / localStorage 全局共享。 // 与 msext threeView 行为对齐(UIWebView 时代无法隔离,外链页可读到游戏会话)。 // 业务上:运营公告 / 充值 / 客服等外链页依赖大厅登录态,必须共享。 config.websiteDataStore = .default() config.defaultWebpagePreferences.allowsContentJavaScript = true config.preferences.javaScriptCanOpenWindowsAutomatically = false config.allowsInlineMediaPlayback = true // .atDocumentEnd 注入 polyfill:与 msext threeView.webViewDidFinishLoad 等价时机 // 用 documentEnd 而不是 documentStart,是为了让外链页自带的 JS 框架先初始化, // 再覆盖 / 创建 window.settings — msext 在 didFinishLoad 才注入,行为等价。 let polyfill = MessageName.allCases.map { name in switch name { case .browser: return "browser:function(u){webkit.messageHandlers.browser.postMessage(String(u||''));}" case .finishweb: return "finishweb:function(){webkit.messageHandlers.finishweb.postMessage('');}" case .backgameData: return "backgameData:function(d){webkit.messageHandlers.backgameData.postMessage(String(d==null?'':d));}" } }.joined(separator: ",") let source = "window.settings=window.settings||{};Object.assign(window.settings,{\(polyfill)});" let userScript = WKUserScript( source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true ) config.userContentController.addUserScript(userScript) // 3 个 MessageHandler 名字与 polyfill 内的 messageHandlers. 一致 for name in MessageName.allCases { config.userContentController.add( ScriptMessageProxy(target: self), name: name.rawValue ) } webView = WKWebView(frame: .zero, configuration: config) webView.scrollView.bounces = false webView.scrollView.contentInsetAdjustmentBehavior = .never webView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(webView) NSLayoutConstraint.activate([ webView.topAnchor.constraint(equalTo: view.topAnchor), webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) } private func loadRequest() { guard let url = URL(string: request.url) else { showFatalAlert(message: "URL 无效:\(request.url)") return } webView.load(URLRequest(url: url)) } // MARK: - Message handling fileprivate func handleMessage(_ name: MessageName, body: Any) { switch name { case .browser: // 系统 Safari 打开(msext threeView.browser: 等价) let raw = (body as? String) ?? "" guard !raw.isEmpty, let target = URL(string: raw) ?? raw.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) .flatMap(URL.init(string:)) else { return } Task { @MainActor in _ = await UIApplication.shared.open(target) } case .finishweb: // 仅 pop,不带数据(settings.finishweb()) AppCoordinator.shared.popOverlay(returningData: nil) case .backgameData: // pop + 发 .subGameDidReturn,父层(大厅 / 子游戏)观察后回调 H5 getWebdata let payload = serialize(body) AppCoordinator.shared.popOverlay(returningData: payload) } } /// 兼容 string / dict / array:dict/array 走 JSON 序列化透传给父层 H5 private func serialize(_ body: Any) -> String { if let s = body as? String { return s } if JSONSerialization.isValidJSONObject(body), let bytes = try? JSONSerialization.data(withJSONObject: body, options: [.fragmentsAllowed]), let s = String(data: bytes, encoding: .utf8) { return s } return "" } private func showFatalAlert(message: String) { let alert = UIAlertController( title: "加载失败", message: message, preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "返回", style: .default) { [weak self] _ in self?.navigationController?.popViewController(animated: true) }) present(alert, animated: true) } } // MARK: - WKScriptMessageHandler proxy // // WKScriptMessageHandler 协议默认 retain 持有者,会跟 WebView 互相强引用导致 VC 不释放。 // 用一个 weak 持有 target 的 proxy 切断循环引用 — msext 时代手动 MRC release 等价做法。 private final class ScriptMessageProxy: NSObject, WKScriptMessageHandler { private weak var target: OverlayViewController? init(target: OverlayViewController) { self.target = target super.init() } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let target, let name = OverlayViewController.MessageName(rawValue: message.name) else { return } target.handleMessage(name, body: message.body) } }