Files
youle_app_ios_v2/ylgamehall/Source/WebView/OverlayViewController.swift
T

209 lines
8.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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-lockedorientation 字段保留但不主动旋转(如有 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.<name>` 一致)。
/// 弹层 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()
// 与大厅 / 子游戏共用默认 websiteDataStorecookie / 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.<name> 一致
for name in MessageName.allCases {
config.userContentController.add(
ScriptMessageProxy(target: self),
name: name.rawValue
)
}
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
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 / arraydict/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)
}
}