diff --git a/ylgamehall/Source/WebView/WebContainerViewController.swift b/ylgamehall/Source/WebView/WebContainerViewController.swift index 5f10e30..fe940f9 100644 --- a/ylgamehall/Source/WebView/WebContainerViewController.swift +++ b/ylgamehall/Source/WebView/WebContainerViewController.swift @@ -188,45 +188,41 @@ public final class WebContainerViewController: UIViewController { /// 在 splash 上展示错误态(不弹 modal alert)。同时启动 NWPathMonitor 静默监听网络 /// 恢复事件,自动触发一次重试。raw error 仍 print 到 Xcode console 便于开发期排查。 + /// + /// 用户点"重试"会直接重跑 runBootPipeline → RemoteConfigClient 重新发起 URLSession + /// 请求。**这正是 Apple 推荐的「让网络请求自然触发权限弹窗」模式**: + /// - 首次启动若系统网络权限弹窗未响应/被拒,重新发请求会再次触发系统对权限状态 + /// 的评估,可能再次弹窗(视 iOS 行为,多数情况下未响应的弹窗会复弹) + /// - 用户在 iOS 隐私管理里改了权限后回 app,点重试就能成功 + /// - NWPathMonitor 也在静默监听,无需用户点重试,切到正常网络后自动重试 private func presentBootError(_ error: any Error) { let kind = Self.classifyBootError(error) print("[Boot] presentBootError kind=\(kind) underlying:", error) let title: String let message: String - let showSettings: Bool switch kind { case .networkOffline: title = "当前未联网" message = "请检查 Wi-Fi 或蜂窝数据后重试。" - showSettings = true case .networkTimeout: title = "网络较慢" message = "连接超时,请稍后重试。" - showSettings = false case .networkCannotReach: title = "暂时无法连接服务器" - message = "网络似乎不太通畅,请稍后重试,或前往系统设置检查网络权限。" - showSettings = true + message = "网络似乎不太通畅,请稍后重试。" case .localResourceMissing: title = "资源加载失败" message = "请尝试重启 app;若仍未恢复,请重新安装。" - showSettings = false case .unknown: title = "启动遇到问题" message = "请稍后重试。" - showSettings = false } splash.onRetry = { [weak self] in Task { @MainActor in await self?.runBootPipeline() } } - splash.onOpenSettings = { - if let url = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(url) - } - } - splash.showError(title: title, message: message, showSettings: showSettings) + splash.showError(title: title, message: message) // 静默后台监听网络恢复,从无网 → 有网时自动重试一次 // (行业主流体验:用户切到 Wi-Fi 后无需手动操作即自动进入大厅) @@ -543,12 +539,9 @@ private final class SplashOverlay: UIView { private let errorTitleLabel = UILabel() private let errorMessageLabel = UILabel() private let retryButton = UIButton(type: .system) - private let settingsButton = UIButton(type: .system) /// 用户点重试按钮时调(启动期 splash 期间) var onRetry: (() -> Void)? - /// 用户点去设置按钮时调 - var onOpenSettings: (() -> Void)? init() { super.init(frame: .zero) @@ -614,44 +607,49 @@ private final class SplashOverlay: UIView { } } - /// 显示错误态。隐藏 label / progress,展示 title + message + 重试 + 去设置(可选)。 - func showError(title: String, message: String, showSettings: Bool) { + /// 显示错误态。隐藏 imageView / label / progress,背景改为浅色系统背景, + /// 展示 title + message + 单个"重试"按钮(点击触发 onRetry)。 + /// 启动图是浅色 + 复杂图案(不适合在上面叠文字),错误态下改纯净浅背景对比清晰。 + func showError(title: String, message: String) { + imageView.isHidden = true label.isHidden = true progressView.isHidden = true + backgroundColor = .systemBackground errorTitleLabel.text = title errorMessageLabel.text = message - settingsButton.isHidden = !showSettings errorContainer.isHidden = false } - /// 关闭错误态,恢复 loading 文案展示(runBootPipeline 重新进入时调)。 + /// 关闭错误态,恢复 splash 默认状态(启动图 + loading 文案;runBootPipeline 重新进入时调)。 func hideError() { errorContainer.isHidden = true + imageView.isHidden = false label.isHidden = false + backgroundColor = .black } private func setupErrorContainer() { - errorTitleLabel.textColor = .white + // 错误态在浅色 systemBackground 上展示,文字用 .label / .secondaryLabel + // 系统语义色(浅模式深、深模式浅),保证两种 iOS appearance 下都看得清。 + errorTitleLabel.textColor = .label errorTitleLabel.font = .systemFont(ofSize: 18, weight: .semibold) errorTitleLabel.textAlignment = .center errorTitleLabel.numberOfLines = 1 - errorMessageLabel.textColor = UIColor.white.withAlphaComponent(0.85) + errorMessageLabel.textColor = .secondaryLabel errorMessageLabel.font = .systemFont(ofSize: 14) errorMessageLabel.textAlignment = .center errorMessageLabel.numberOfLines = 0 - configureFilledButton(retryButton, title: "重试") - configureOutlineButton(settingsButton, title: "前往系统设置") + var cfg = UIButton.Configuration.filled() + cfg.title = "重试" + cfg.baseBackgroundColor = .systemBlue + cfg.baseForegroundColor = .white + cfg.cornerStyle = .medium + cfg.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24) + retryButton.configuration = cfg retryButton.addAction(UIAction { [weak self] _ in self?.onRetry?() }, for: .touchUpInside) - settingsButton.addAction(UIAction { [weak self] _ in self?.onOpenSettings?() }, for: .touchUpInside) - - let buttonStack = UIStackView(arrangedSubviews: [retryButton, settingsButton]) - buttonStack.axis = .horizontal - buttonStack.spacing = 12 - buttonStack.alignment = .center - buttonStack.distribution = .fillEqually errorContainer.axis = .vertical errorContainer.alignment = .center @@ -660,36 +658,12 @@ private final class SplashOverlay: UIView { errorContainer.translatesAutoresizingMaskIntoConstraints = false errorContainer.addArrangedSubview(errorTitleLabel) errorContainer.addArrangedSubview(errorMessageLabel) - errorContainer.addArrangedSubview(buttonStack) + errorContainer.addArrangedSubview(retryButton) addSubview(errorContainer) NSLayoutConstraint.activate([ retryButton.heightAnchor.constraint(equalToConstant: 40), - retryButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96), - settingsButton.heightAnchor.constraint(equalToConstant: 40), - settingsButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 130) + retryButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) ]) } - - private func configureFilledButton(_ button: UIButton, title: String) { - var cfg = UIButton.Configuration.filled() - cfg.title = title - cfg.baseBackgroundColor = .systemBlue - cfg.baseForegroundColor = .white - cfg.cornerStyle = .medium - cfg.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 20, bottom: 8, trailing: 20) - button.configuration = cfg - } - - private func configureOutlineButton(_ button: UIButton, title: String) { - var cfg = UIButton.Configuration.bordered() - cfg.title = title - cfg.baseForegroundColor = .white - cfg.cornerStyle = .medium - cfg.background.strokeColor = .white - cfg.background.strokeWidth = 1 - cfg.background.backgroundColor = .clear - cfg.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) - button.configuration = cfg - } }