// // 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) ]) } }