SplashOverlay 抽公共组件 + 进度条配色统一

大厅 / 子游戏之前各有一份 private SplashOverlay:lobby 版含错误态 UI(拉远端
配置失败 splash 上提示重试 / 前往设置);sub-game 版只有 image + label +
progressView,配色规则不同(width 0.45 vs 0.7,progressTintColor 不设)。

抽到 Source/WebView/SplashOverlay.swift 共用组件:
- 含完整加载 UI + 错误态 UI(sub-game 不调 showError,但保留兜底)
- 进度条 progressTintColor = .black,trackTintColor = .systemGray5(实色浅灰)
  之前 lobby 用 .systemBlue + label.withAlphaComponent(0.15) 半透明轨道,
  黑色填充推过半透明轨道时视觉上像"颜色逐渐加深";改实色轨道后是干脆的
  "黑滑过灰",无加深错觉
- progressView.setProgress(animated: true) 保留补间,让快下载也能看到平滑增长

WebContainer / SubGame 删除各自的 private SplashOverlay 类定义,引用同一份。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-23 21:34:25 +08:00
co-authored by Claude Opus 4.7
parent e50dfb75af
commit 57d4d93781
3 changed files with 168 additions and 217 deletions
@@ -0,0 +1,168 @@
//
// SplashOverlay.swift
// ylgamehall
//
// 与 LaunchScreen 同款启动图 + UIProgressView + 状态文字标签的复合 view,
// 大厅 / 子游戏 boot pipeline 期间覆盖在 WebView 上方,加载完成 fade out 后移除。
//
// 内置错误态 UI(title + message + 单按钮):仅大厅启动期使用——拉远端配置 /
// 解压 / IPA 升级失败时调 showError(...),按错误 kind 切换"重试" / "前往设置"
// 按钮语义;子游戏目前不调用,沿用 fatal alert 路径。
//
import UIKit
final class SplashOverlay: UIView {
private let imageView = UIImageView()
private let progressView = UIProgressView(progressViewStyle: .default)
private let label = UILabel()
// 错误态 UI(默认隐藏;showError 时显示,覆盖在 label/progress 区域)
private let errorContainer = UIStackView()
private let errorTitleLabel = UILabel()
private let errorMessageLabel = UILabel()
private let retryButton = UIButton(type: .system)
/// 用户点重试按钮时调(启动期 splash 期间)
var onRetry: (() -> Void)?
init() {
super.init(frame: .zero)
backgroundColor = .black
// 与 LaunchScreen.storyboard 同款 contentMode = scaleAspectFill:
// 素材是 1136×640(16:9)横屏画面,现代 iPhone 横屏约 19.5:9 比 16:9 更宽,
// scaleAspectFill 高度撑满 + 宽度按比例放大→左右轻微裁切;启动图左右是空白底色,
// logo + 文字主体居中可见,裁切无影响。
// 等价 daoqi msext LaunchImage Asset Catalog 时代「启动图覆盖整屏」的体感,
// 避免 scaleAspectFit 在现代屏左右补大黑边。
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "SplashImage")
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
label.textColor = .white
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textAlignment = .center
label.numberOfLines = 1
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.isHidden = true
// 进度条填充黑色 + 轨道用**不透明**浅灰(systemGray5)。
// 之前用 label.withAlphaComponent(0.15) 半透明轨道,会透出 splash 启动图的颜色,
// 黑色填充推进时视觉上像"把彩色压成黑色 → 颜色逐渐加深"。改实色轨道后两侧都是
// 实色,过渡是干脆的"黑色滑过灰色",无加深错觉。
progressView.progressTintColor = .black
progressView.trackTintColor = .systemGray5
addSubview(progressView)
setupErrorContainer()
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -32),
label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -24),
progressView.centerXAnchor.constraint(equalTo: centerXAnchor),
progressView.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -12),
progressView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.7),
errorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
errorContainer.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 60),
errorContainer.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
errorContainer.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -24),
errorContainer.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, multiplier: 0.7)
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("SplashOverlay does not support init(coder:)") }
func update(text: String, progress: Double?) {
label.text = text
if let p = progress {
progressView.isHidden = false
// 用 animated:true 让填充宽度在两个 progress 值之间平滑过渡。
// 下载回调来得快(甚至同一 RunLoop 多次),不动画会出现"看不到中间帧、
// 一瞬间跳到 100%"的视觉。animated:true 是 UIKit 的标准进度条用法。
progressView.setProgress(Float(p), animated: true)
} else {
progressView.isHidden = true
progressView.setProgress(0, animated: false)
}
}
/// 显示错误态。隐藏 imageView / label / progress,背景改为浅色系统背景,
/// 展示 title + message + 单个按钮(点击触发 onRetry)。
/// 按钮文字由调用方按错误 kind 决定("重试" / "前往设置"),同一个按钮承担双语义。
/// 启动图是浅色 + 复杂图案(不适合在上面叠文字),错误态下改纯净浅背景对比清晰。
func showError(title: String, message: String, actionTitle: String = "重试") {
imageView.isHidden = true
label.isHidden = true
progressView.isHidden = true
backgroundColor = .systemBackground
errorTitleLabel.text = title
errorMessageLabel.text = message
var cfg = retryButton.configuration
cfg?.title = actionTitle
retryButton.configuration = cfg
errorContainer.isHidden = false
}
/// 关闭错误态,恢复 splash 默认状态(启动图 + loading 文案;runBootPipeline 重新进入时调)。
func hideError() {
errorContainer.isHidden = true
imageView.isHidden = false
label.isHidden = false
backgroundColor = .black
}
private func setupErrorContainer() {
// 错误态在浅色 systemBackground 上展示,文字用 .label / .secondaryLabel
// 系统语义色(浅模式深、深模式浅),保证两种 iOS appearance 下都看得清。
errorTitleLabel.textColor = .label
errorTitleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
errorTitleLabel.textAlignment = .center
errorTitleLabel.numberOfLines = 1
errorMessageLabel.textColor = .secondaryLabel
errorMessageLabel.font = .systemFont(ofSize: 14)
errorMessageLabel.textAlignment = .center
errorMessageLabel.numberOfLines = 0
var cfg = UIButton.Configuration.filled()
cfg.title = "重试"
cfg.baseBackgroundColor = .black
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)
errorContainer.axis = .vertical
errorContainer.alignment = .center
errorContainer.spacing = 12
errorContainer.isHidden = true
errorContainer.translatesAutoresizingMaskIntoConstraints = false
errorContainer.addArrangedSubview(errorTitleLabel)
errorContainer.addArrangedSubview(errorMessageLabel)
errorContainer.addArrangedSubview(retryButton)
addSubview(errorContainer)
NSLayoutConstraint.activate([
retryButton.heightAnchor.constraint(equalToConstant: 40),
retryButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120)
])
}
}
@@ -364,66 +364,3 @@ extension SubGameViewController: WKNavigationDelegate {
} }
} }
// MARK: - SplashOverlay(与 WebContainerViewController 同款,先就地复制;
// Phase 1.B 后续抽公共组件时合并到 Source/UI/SplashOverlay.swift)
private final class SplashOverlay: UIView {
private let imageView = UIImageView()
private let progressView = UIProgressView(progressViewStyle: .default)
private let label = UILabel()
init() {
super.init(frame: .zero)
backgroundColor = .black
// scaleAspectFill:与 LaunchScreen / WebContainer splash 同款全屏覆盖策略
// (现代 iPhone 横屏 19.5:9 比 16:9 素材更宽,scaleAspectFit 会左右补大黑边)
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "SplashImage")
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
label.textColor = .white
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textAlignment = .center
label.numberOfLines = 1
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.isHidden = true
addSubview(progressView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -32),
label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -24),
progressView.centerXAnchor.constraint(equalTo: centerXAnchor),
progressView.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -12),
progressView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.45)
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("SplashOverlay does not support init(coder:)") }
func update(text: String, progress: Double?) {
label.text = text
if let p = progress {
progressView.isHidden = false
progressView.setProgress(Float(p), animated: true)
} else {
progressView.isHidden = true
progressView.setProgress(0, animated: false)
}
}
}
@@ -610,157 +610,3 @@ extension WebContainerViewController: WKNavigationDelegate {
} }
} }
// MARK: - SplashOverlay
//
// 与 LaunchScreen 同款启动图 + UIProgressView + 状态文字标签的复合 view。
// 单文件私有:仅 WebContainerViewController 使用;若 Phase 6 子游戏容器复用,再抽出。
private final class SplashOverlay: UIView {
private let imageView = UIImageView()
private let progressView = UIProgressView(progressViewStyle: .default)
private let label = UILabel()
// 错误态 UI(默认隐藏;showError 时显示,覆盖在 label/progress 区域)
private let errorContainer = UIStackView()
private let errorTitleLabel = UILabel()
private let errorMessageLabel = UILabel()
private let retryButton = UIButton(type: .system)
/// 用户点重试按钮时调(启动期 splash 期间)
var onRetry: (() -> Void)?
init() {
super.init(frame: .zero)
backgroundColor = .black
// 与 LaunchScreen.storyboard 同款 contentMode = scaleAspectFill:
// 素材是 1136×640(16:9)横屏画面,现代 iPhone 横屏约 19.5:9 比 16:9 更宽,
// scaleAspectFill 高度撑满 + 宽度按比例放大→左右轻微裁切;启动图左右是空白底色,
// logo + 文字主体居中可见,裁切无影响。
// 等价 daoqi msext LaunchImage Asset Catalog 时代「启动图覆盖整屏」的体感,
// 避免 scaleAspectFit 在现代屏左右补大黑边。
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "SplashImage")
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
label.textColor = .white
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textAlignment = .center
label.numberOfLines = 1
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.isHidden = true
// 与错误态主按钮("重试")配色一致,进度条用 systemBlue;
// 轨道用 .label 的 15% alpha,浅深模式自适应(浅模式淡灰、深模式淡白)
progressView.progressTintColor = .systemBlue
progressView.trackTintColor = UIColor.label.withAlphaComponent(0.15)
addSubview(progressView)
setupErrorContainer()
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -32),
label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -24),
progressView.centerXAnchor.constraint(equalTo: centerXAnchor),
progressView.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -12),
progressView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.7),
errorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
errorContainer.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 60),
errorContainer.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
errorContainer.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -24),
errorContainer.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, multiplier: 0.7)
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("SplashOverlay does not support init(coder:)") }
func update(text: String, progress: Double?) {
label.text = text
if let p = progress {
progressView.isHidden = false
progressView.setProgress(Float(p), animated: true)
} else {
progressView.isHidden = true
progressView.setProgress(0, animated: false)
}
}
/// 显示错误态。隐藏 imageView / label / progress,背景改为浅色系统背景,
/// 展示 title + message + 单个按钮(点击触发 onRetry)。
/// 按钮文字由调用方按错误 kind 决定("重试" / "前往设置"),同一个按钮承担双语义。
/// 启动图是浅色 + 复杂图案(不适合在上面叠文字),错误态下改纯净浅背景对比清晰。
func showError(title: String, message: String, actionTitle: String = "重试") {
imageView.isHidden = true
label.isHidden = true
progressView.isHidden = true
backgroundColor = .systemBackground
errorTitleLabel.text = title
errorMessageLabel.text = message
var cfg = retryButton.configuration
cfg?.title = actionTitle
retryButton.configuration = cfg
errorContainer.isHidden = false
}
/// 关闭错误态,恢复 splash 默认状态(启动图 + loading 文案;runBootPipeline 重新进入时调)。
func hideError() {
errorContainer.isHidden = true
imageView.isHidden = false
label.isHidden = false
backgroundColor = .black
}
private func setupErrorContainer() {
// 错误态在浅色 systemBackground 上展示,文字用 .label / .secondaryLabel
// 系统语义色(浅模式深、深模式浅),保证两种 iOS appearance 下都看得清。
errorTitleLabel.textColor = .label
errorTitleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
errorTitleLabel.textAlignment = .center
errorTitleLabel.numberOfLines = 1
errorMessageLabel.textColor = .secondaryLabel
errorMessageLabel.font = .systemFont(ofSize: 14)
errorMessageLabel.textAlignment = .center
errorMessageLabel.numberOfLines = 0
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)
errorContainer.axis = .vertical
errorContainer.alignment = .center
errorContainer.spacing = 12
errorContainer.isHidden = true
errorContainer.translatesAutoresizingMaskIntoConstraints = false
errorContainer.addArrangedSubview(errorTitleLabel)
errorContainer.addArrangedSubview(errorMessageLabel)
errorContainer.addArrangedSubview(retryButton)
addSubview(errorContainer)
NSLayoutConstraint.activate([
retryButton.heightAnchor.constraint(equalToConstant: 40),
retryButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120)
])
}
}