启动期错误处理 v2:撤回降级,splash 错误态 + NWPathMonitor 自动重试
撤回上一 commit 74f6925 的"远端 config 失败就降级进大厅"路径——错过升级
判定 = 用户用旧 IPA 不被强制升级 = 业务风险。新策略:
**远端 config 失败 → 停留 splash 显示错误态**(不弹 modal alert / 不暴露原始 NSError)
行业主流方案:
1. SplashOverlay 扩展错误态 UI(隐藏 label / progress,显示 title +
message + "重试" 主按钮 + "前往系统设置" 次按钮),通过 onRetry /
onOpenSettings closure 与 controller 解耦
2. WebContainerViewController.presentBootError 按 BootErrorKind 分类给出
友好文案(networkOffline / networkTimeout / networkCannotReach /
localResourceMissing / unknown)
3. 启动期独立 NWPathMonitor,仅在"从无网变有网"时静默自动重试一次
(初始 nil 状态不触发,避免死循环;用户切到 Wi-Fi 后无需手动操作)
4. runBootPipeline 重新进入时 stopBootRetryWaiting + splash.hideError
保证多次重试干净(防止 monitor 泄漏)
进大厅的必要条件(恢复严格):
- ResourceUnzipper.ensureReady 成功(本地 H5 zip 就位)
- RemoteConfigClient.fetch 拿到正确 config(升级判定可执行)
- 本地 appVersion >= 远端(否则走 IPA 升级 modal alert)
- showmessage 为空(否则走运营 modal alert)
- LobbyZipUpgrader 完成(含跳过升级 / 真升级两路径)
业务流程信号(operationalMessage / ipaUpgradeRequired)继续用 modal alert
(这两类不是错误,是产品决策的弹窗永停);技术错误(网络/解压/写文件等)
统一进 splash 错误态 + 自动重试。
raw error 仍 print 到 Xcode console(含 NSURLErrorDomain code 等开发期
排查信息),用户视角永不暴露技术细节。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
74f6925be3
commit
5731588d33
@@ -9,6 +9,7 @@
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
import Network
|
||||
|
||||
public final class WebContainerViewController: UIViewController {
|
||||
|
||||
@@ -164,108 +165,165 @@ public final class WebContainerViewController: UIViewController {
|
||||
// MARK: - Boot pipeline
|
||||
|
||||
private func runBootPipeline() async {
|
||||
// 重新进入 pipeline 时(重试路径):重置 splash 并显示
|
||||
// 重新进入 pipeline 时(重试路径):清除错误态 + 重置 splash
|
||||
stopBootRetryWaiting()
|
||||
splash.hideError()
|
||||
splash.isHidden = false
|
||||
splash.update(text: "拼命启动中...", progress: nil)
|
||||
|
||||
do {
|
||||
try await runBootPipelineSteps()
|
||||
} catch BootError.operationalMessage(let msg) {
|
||||
// 运营杀手锏:modal alert(业务流程信号,不是技术错误)
|
||||
showBlockingAlert(message: msg)
|
||||
} catch BootError.ipaUpgradeRequired(let dl) {
|
||||
// IPA 升级 modal alert(业务流程信号)
|
||||
showIPAUpgradeAlert(downloadURL: dl)
|
||||
} catch {
|
||||
// 走到这里说明:优雅降级也没救(多半是 ResourceUnzipper.ensureReady 失败 /
|
||||
// writeAppDataFiles 文件系统失败 等本地资源问题,因为网络相关错误已经在
|
||||
// runBootPipelineSteps 内被 fallback 吞掉了)。仍按友好文案弹窗,不暴露原始 NSError。
|
||||
showRetryableAlert(error: error)
|
||||
// 网络 / 本地资源 / 解析等可重试错误 → 在 splash 上展示错误态
|
||||
// (不弹 modal、不暴露原始 NSError),并启动 NWPathMonitor 静默自动重试
|
||||
presentBootError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// 在 splash 上展示错误态(不弹 modal alert)。同时启动 NWPathMonitor 静默监听网络
|
||||
/// 恢复事件,自动触发一次重试。raw error 仍 print 到 Xcode console 便于开发期排查。
|
||||
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
|
||||
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)
|
||||
|
||||
// 静默后台监听网络恢复,从无网 → 有网时自动重试一次
|
||||
// (行业主流体验:用户切到 Wi-Fi 后无需手动操作即自动进入大厅)
|
||||
startBootRetryWaiting()
|
||||
}
|
||||
|
||||
/// 启动期 NWPathMonitor。仅在 splash 错误态期间存活;进入大厅或用户主动重试时销毁。
|
||||
/// 仅在"从无网变有网"时触发自动重试,避免初始 satisfied 状态导致死循环。
|
||||
private var bootRetryPathMonitor: NWPathMonitor?
|
||||
|
||||
private func startBootRetryWaiting() {
|
||||
stopBootRetryWaiting()
|
||||
let m = NWPathMonitor()
|
||||
let queue = DispatchQueue(label: "ylgamehall.boot-retry", qos: .utility)
|
||||
nonisolated(unsafe) var previousSatisfied: Bool? = nil
|
||||
m.pathUpdateHandler = { [weak self] path in
|
||||
let isSatisfied = (path.status == .satisfied)
|
||||
let prev = previousSatisfied
|
||||
previousSatisfied = isSatisfied
|
||||
// 仅从 false 转 true 触发自动重试;初始 nil(首次回调)不触发
|
||||
guard prev == false, isSatisfied else { return }
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
print("[Boot] 网络恢复,自动重试启动流水线")
|
||||
await self.runBootPipeline()
|
||||
}
|
||||
}
|
||||
m.start(queue: queue)
|
||||
bootRetryPathMonitor = m
|
||||
}
|
||||
|
||||
private func stopBootRetryWaiting() {
|
||||
bootRetryPathMonitor?.cancel()
|
||||
bootRetryPathMonitor = nil
|
||||
}
|
||||
|
||||
private func runBootPipelineSteps() async throws {
|
||||
// 1. 首装解压(仅首次安装走真正的 unzip;之后 fileExists 短路 ≤ 1ms)
|
||||
try await ResourceUnzipper.shared.ensureReady()
|
||||
|
||||
// 2-5. 拉远端配置 + 升级判定,整段优雅降级:
|
||||
// - 网络 / 解析 / 下载失败 → 不抛错,用本地 LocalVersionReader 构造 fallback
|
||||
// ResolvedVersion,跳过升级判定直接走本地 H5
|
||||
// - 业务流程信号(operationalMessage / ipaUpgradeRequired)正常抛出由外层弹窗
|
||||
// 2. 拉远端渠道配置(**强约束**:必须拿到正确远端 config 才进大厅)
|
||||
//
|
||||
// 行业主流做法:远端 config 失败 ≠ 启动失败。本地 zip 在 ensureReady 之后已就绪,
|
||||
// 没有远端 config 只是失去"能否升级判定",大厅 H5 本身完全可加载。这避免了用户
|
||||
// 在 iOS 14+ 网络权限弹窗未响应 / 测试环境域名不可达 / 临时断网等场景下被
|
||||
// "启动失败 + 原始 NSError" 卡死的差体验。
|
||||
// 关键决策:**不能**在远端 config 失败时走 fallback 直接进大厅。
|
||||
// 原因:远端 config 包含「本地 appVersion 是否落后于远端」的升级判定,
|
||||
// 跳过 = 用户用旧版本不被强制升级 = 业务风险。
|
||||
//
|
||||
// 失败处理 UX(行业主流:停在 splash 显示错误态 + 自动重试):
|
||||
// - 抛出网络错误 → 外层 runBootPipeline catch → splash.showError
|
||||
// - 同时启动 NWPathMonitor,网络从无变有时自动重试一次
|
||||
// - 用户也可手动点 splash 上的「重试」/「前往系统设置」按钮
|
||||
// - 永不弹 modal alert / 永不暴露原始 NSError
|
||||
splash.update(text: "拉取配置中...", progress: nil)
|
||||
|
||||
let resolved: ResolvedVersion
|
||||
let usedOfflineFallback: Bool
|
||||
let outcome = try await RemoteConfigClient.shared.fetch()
|
||||
switch outcome {
|
||||
case .shortText(let msg):
|
||||
// 运营杀手锏 #1:服务端响应被替换为短文本,前端弹窗永停
|
||||
throw BootError.operationalMessage(msg)
|
||||
|
||||
do {
|
||||
let outcome = try await RemoteConfigClient.shared.fetch()
|
||||
switch outcome {
|
||||
case .shortText(let msg):
|
||||
// 运营杀手锏 #1:服务端响应被替换为短文本,前端弹窗永停
|
||||
throw BootError.operationalMessage(msg)
|
||||
|
||||
case .parsed(let cfg):
|
||||
let bc = BundleConfig.shared
|
||||
let r = VersionResolver.resolve(
|
||||
config: cfg,
|
||||
agentId: bc.agent,
|
||||
channelId: bc.channel,
|
||||
marketId: bc.market,
|
||||
gameId: bc.gameId
|
||||
)
|
||||
|
||||
// 运营杀手锏 #2:showmessage 非空 → 弹窗永停
|
||||
if let msg = r.showmessage, !msg.isEmpty {
|
||||
throw BootError.operationalMessage(msg)
|
||||
}
|
||||
|
||||
// IPA 升级(远端 appVersion > 本地):弹窗 + Safari 外链永停
|
||||
if r.appVersion > LocalVersionReader.localAppVersion,
|
||||
let dl = r.appDownload {
|
||||
throw BootError.ipaUpgradeRequired(downloadURL: dl)
|
||||
}
|
||||
|
||||
// H5 zip 升级失败也走降级(继续用本地版本,不阻塞启动;
|
||||
// LobbyZipUpgrader.onProgress 由 URLSession delegate 在后台队列调度,
|
||||
// 必须显式 hop 到 MainActor 才能动 splash UI)
|
||||
do {
|
||||
_ = try await LobbyZipUpgrader.shared.upgradeIfNeeded(
|
||||
resolved: r,
|
||||
onProgress: { [weak self] p in
|
||||
Task { @MainActor in
|
||||
self?.splash.update(
|
||||
text: String(format: "下载更新中 %d%%", Int(p * 100)),
|
||||
progress: p
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
print("[Boot] H5 zip 升级失败,继续用本地资源:", error)
|
||||
}
|
||||
|
||||
resolved = r
|
||||
usedOfflineFallback = false
|
||||
}
|
||||
} catch let e as BootError {
|
||||
// 业务流程信号继续抛(外层弹 operational / IPA 升级弹窗)
|
||||
throw e
|
||||
} catch {
|
||||
// 远端 config 失败(多数是网络),优雅降级用本地版本继续启动。
|
||||
// raw error 打到 Xcode console 便于排查(含 NSURLErrorDomain code 等);
|
||||
// 用户感知仅是 splash 多停一会儿 → 直接进入大厅,无致命弹窗。
|
||||
print("[Boot] 远端配置拉取失败,降级为离线模式继续启动:", error)
|
||||
resolved = ResolvedVersion(
|
||||
appVersion: LocalVersionReader.localAppVersion,
|
||||
appDownload: nil,
|
||||
gameVersion: LocalVersionReader.localGameVersion,
|
||||
gameZip: nil,
|
||||
showmessage: nil
|
||||
case .parsed(let cfg):
|
||||
let bc = BundleConfig.shared
|
||||
let r = VersionResolver.resolve(
|
||||
config: cfg,
|
||||
agentId: bc.agent,
|
||||
channelId: bc.channel,
|
||||
marketId: bc.market,
|
||||
gameId: bc.gameId
|
||||
)
|
||||
usedOfflineFallback = true
|
||||
|
||||
// 运营杀手锏 #2:showmessage 非空 → 弹窗永停
|
||||
if let msg = r.showmessage, !msg.isEmpty {
|
||||
throw BootError.operationalMessage(msg)
|
||||
}
|
||||
|
||||
// IPA 升级(远端 appVersion > 本地):弹窗 + Safari 外链永停
|
||||
if r.appVersion > LocalVersionReader.localAppVersion,
|
||||
let dl = r.appDownload {
|
||||
throw BootError.ipaUpgradeRequired(downloadURL: dl)
|
||||
}
|
||||
|
||||
// H5 zip 升级失败仍致命(用户体验 ≠ 静默用旧 zip)—— 抛出由外层 splash.showError
|
||||
// LobbyZipUpgrader.onProgress 由 URLSession delegate 后台队列调度,
|
||||
// 必须显式 hop 到 MainActor 才能动 splash UI。
|
||||
_ = try await LobbyZipUpgrader.shared.upgradeIfNeeded(
|
||||
resolved: r,
|
||||
onProgress: { [weak self] p in
|
||||
Task { @MainActor in
|
||||
self?.splash.update(
|
||||
text: String(format: "下载更新中 %d%%", Int(p * 100)),
|
||||
progress: p
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
resolved = r
|
||||
}
|
||||
|
||||
// 6. 在 loadFileURL 之前写 4 个 app_*.js(与原 msext NewRootVC.initJSdata 等价时序)
|
||||
@@ -273,8 +331,7 @@ public final class WebContainerViewController: UIViewController {
|
||||
try writeAppDataFiles(resolved: resolved)
|
||||
|
||||
// 7. 加载本地 H5(allowingReadAccessTo 必须给 lobbyRoot 才能跨目录引用资源)
|
||||
// splash 文案区分在线/离线,让用户对状态有感知(不暴露技术细节)
|
||||
splash.update(text: usedOfflineFallback ? "离线加载..." : "加载大厅...", progress: nil)
|
||||
splash.update(text: "加载大厅...", progress: nil)
|
||||
bridgedWebView.webView.loadFileURL(
|
||||
SandboxPaths.lobbyIndex,
|
||||
allowingReadAccessTo: SandboxPaths.lobbyRoot
|
||||
@@ -415,67 +472,9 @@ public final class WebContainerViewController: UIViewController {
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 启动期可重试错误的友好弹窗。
|
||||
///
|
||||
/// **不暴露原始 NSError 给用户**(如 `Error Domain=NSURLErrorDomain Code=-1009 ...`)。
|
||||
/// 按错误分类(剥洋葱看 NSURLError 子类型)给出友好标题 + 正文 + 适配的按钮组合:
|
||||
/// - 网络类(offline / timeout / cannotReach)→ 重试 + 去设置 + 稍后再说
|
||||
/// - 本地资源类(unzip / 写文件失败)→ 重试 + 稍后再说
|
||||
/// - 未知 → 通用文案
|
||||
///
|
||||
/// raw error 仍打到 Xcode console 便于开发期排查;用户看不到技术细节。
|
||||
///
|
||||
/// **重要**:runBootPipelineSteps 已对网络失败做了优雅降级,本方法实际只会在
|
||||
/// 本地资源问题时触发。但仍保留网络分类分支,以防未来某条 fallback 漏开 catch。
|
||||
private func showRetryableAlert(error: Error) {
|
||||
let kind = Self.classifyBootError(error)
|
||||
print("[Boot] showRetryableAlert kind=\(kind) underlying:", error)
|
||||
|
||||
let title: String
|
||||
let message: String
|
||||
let showSettingsAction: Bool
|
||||
switch kind {
|
||||
case .networkOffline:
|
||||
title = "网络未连接"
|
||||
message = "请检查 Wi-Fi 或蜂窝数据后重试。"
|
||||
showSettingsAction = true
|
||||
case .networkTimeout:
|
||||
title = "网络较慢"
|
||||
message = "连接超时,请稍后重试。"
|
||||
showSettingsAction = false
|
||||
case .networkCannotReach:
|
||||
title = "暂时无法连接服务器"
|
||||
message = "网络似乎不太通畅,请稍后重试,或前往设置检查网络。"
|
||||
showSettingsAction = true
|
||||
case .localResourceMissing:
|
||||
title = "资源加载失败"
|
||||
message = "请尝试重启 app;若仍未恢复,请重新安装。"
|
||||
showSettingsAction = false
|
||||
case .unknown:
|
||||
title = "启动遇到问题"
|
||||
message = "请稍后重试。"
|
||||
showSettingsAction = false
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "重试", style: .default) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
await self?.runBootPipeline()
|
||||
}
|
||||
})
|
||||
if showSettingsAction {
|
||||
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "稍后再说", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 启动期错误的分类。剥 RemoteConfigError.allRetriesFailed 找到 underlying NSURLError,
|
||||
/// 按 NSURLErrorDomain code 映射到友好类别;非 NSURLError 视作本地资源问题。
|
||||
/// 给 `presentBootError` 决定 splash 错误态的标题/正文/按钮组合用。
|
||||
private enum BootErrorKind {
|
||||
case networkOffline
|
||||
case networkTimeout
|
||||
@@ -539,6 +538,18 @@ private final class SplashOverlay: UIView {
|
||||
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)
|
||||
private let settingsButton = UIButton(type: .system)
|
||||
|
||||
/// 用户点重试按钮时调(启动期 splash 期间)
|
||||
var onRetry: (() -> Void)?
|
||||
/// 用户点去设置按钮时调
|
||||
var onOpenSettings: (() -> Void)?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .black
|
||||
@@ -564,6 +575,8 @@ private final class SplashOverlay: UIView {
|
||||
progressView.isHidden = true
|
||||
addSubview(progressView)
|
||||
|
||||
setupErrorContainer()
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
@@ -577,7 +590,13 @@ private final class SplashOverlay: UIView {
|
||||
|
||||
progressView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
progressView.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -12),
|
||||
progressView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.45)
|
||||
progressView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.45),
|
||||
|
||||
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)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -594,4 +613,83 @@ private final class SplashOverlay: UIView {
|
||||
progressView.setProgress(0, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示错误态。隐藏 label / progress,展示 title + message + 重试 + 去设置(可选)。
|
||||
func showError(title: String, message: String, showSettings: Bool) {
|
||||
label.isHidden = true
|
||||
progressView.isHidden = true
|
||||
errorTitleLabel.text = title
|
||||
errorMessageLabel.text = message
|
||||
settingsButton.isHidden = !showSettings
|
||||
errorContainer.isHidden = false
|
||||
}
|
||||
|
||||
/// 关闭错误态,恢复 loading 文案展示(runBootPipeline 重新进入时调)。
|
||||
func hideError() {
|
||||
errorContainer.isHidden = true
|
||||
label.isHidden = false
|
||||
}
|
||||
|
||||
private func setupErrorContainer() {
|
||||
errorTitleLabel.textColor = .white
|
||||
errorTitleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
|
||||
errorTitleLabel.textAlignment = .center
|
||||
errorTitleLabel.numberOfLines = 1
|
||||
|
||||
errorMessageLabel.textColor = UIColor.white.withAlphaComponent(0.85)
|
||||
errorMessageLabel.font = .systemFont(ofSize: 14)
|
||||
errorMessageLabel.textAlignment = .center
|
||||
errorMessageLabel.numberOfLines = 0
|
||||
|
||||
configureFilledButton(retryButton, title: "重试")
|
||||
configureOutlineButton(settingsButton, title: "前往系统设置")
|
||||
|
||||
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
|
||||
errorContainer.spacing = 12
|
||||
errorContainer.isHidden = true
|
||||
errorContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
errorContainer.addArrangedSubview(errorTitleLabel)
|
||||
errorContainer.addArrangedSubview(errorMessageLabel)
|
||||
errorContainer.addArrangedSubview(buttonStack)
|
||||
addSubview(errorContainer)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
retryButton.heightAnchor.constraint(equalToConstant: 40),
|
||||
retryButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96),
|
||||
settingsButton.heightAnchor.constraint(equalToConstant: 40),
|
||||
settingsButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 130)
|
||||
])
|
||||
}
|
||||
|
||||
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