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:
co-authored by
Claude Opus 4.7
parent
e50dfb75af
commit
57d4d93781
@@ -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)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user