diff --git a/ylgamehall/Source/WebView/SplashOverlay.swift b/ylgamehall/Source/WebView/SplashOverlay.swift new file mode 100644 index 0000000..e5efda3 --- /dev/null +++ b/ylgamehall/Source/WebView/SplashOverlay.swift @@ -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) + ]) + } +} diff --git a/ylgamehall/Source/WebView/SubGameViewController.swift b/ylgamehall/Source/WebView/SubGameViewController.swift index 33284ac..601457d 100644 --- a/ylgamehall/Source/WebView/SubGameViewController.swift +++ b/ylgamehall/Source/WebView/SubGameViewController.swift @@ -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) - } - } -} diff --git a/ylgamehall/Source/WebView/WebContainerViewController.swift b/ylgamehall/Source/WebView/WebContainerViewController.swift index cbb89d4..a18f8e7 100644 --- a/ylgamehall/Source/WebView/WebContainerViewController.swift +++ b/ylgamehall/Source/WebView/WebContainerViewController.swift @@ -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) - ]) - } -}