From 34f76e882457caf27835f6de070570fde189c8c5 Mon Sep 17 00:00:00 2001 From: joywayer Date: Tue, 23 Jun 2026 07:10:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=9C=9F=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=80=81=20UX=20=E8=B0=83=E6=95=B4=EF=BC=9A=E6=B5=85=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E6=B7=B1=E8=89=B2=E5=AD=97=EF=BC=8C=E4=BB=85=E4=BF=9D?= =?UTF-8?q?=E7=95=99"=E9=87=8D=E8=AF=95"=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按用户反馈精简错误态 UI: - splash 启动图是浅色背景(米白底 + logo + 蓝色文字),白色文字在上面 看不清。错误态显示时改为 backgroundColor = .systemBackground(浅模式 白、深模式黑),同时隐藏启动图 imageView,文字用 .label / .secondaryLabel 系统语义色确保两种 appearance 下都对比清晰 - 去掉"前往系统设置"次按钮:仅保留蓝色填充主按钮"重试" - "无法连接服务器"文案删去"或前往系统设置"尾句 "权限弹窗"诉求的实际实现: 重试按钮直接重跑 runBootPipeline → RemoteConfigClient.fetch → URLSession.dataTask 重新发起请求。这正是 Apple 推荐的"让网络请求 自然触发权限弹窗"模式: - 首次启动若系统网络权限弹窗未响应/被拒,重新发请求会再次触发系统 对权限状态的评估,可能复弹 - 用户在 iOS 隐私管理里改了权限后回 app,点重试就能成功 - NWPathMonitor 也在静默监听,切到正常网络后无需点击即自动重试 不需要额外的"手动申请权限" API 代码(iOS 没有公开的 API 强制弹起 被拒绝的网络权限弹窗);现有「重新发请求」即是教科书做法。 Co-Authored-By: Claude Opus 4.7 --- .../WebView/WebContainerViewController.swift | 88 +++++++------------ 1 file changed, 31 insertions(+), 57 deletions(-) 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 - } }