启动期错误态 UX 调整:浅背景深色字,仅保留"重试"按钮
按用户反馈精简错误态 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5731588d33
commit
34f76e8824
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user