Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
5.9 KiB
Swift
187 lines
5.9 KiB
Swift
//
|
||
// ToastCenter.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/18.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
@MainActor
|
||
/// 全局 Toast 状态中心,负责保存和清除当前提示文案。
|
||
final class ToastCenter {
|
||
var onChange: (() -> Void)?
|
||
private(set) var message: String? { didSet { onChange?() } }
|
||
|
||
private let autoDismissNanoseconds: UInt64
|
||
private var dismissTask: Task<Void, Never>?
|
||
private var displayToken: UInt64 = 0
|
||
private weak var overlayView: ToastOverlayView?
|
||
|
||
/// 创建 Toast 状态中心,默认 2.2 秒后自动隐藏。
|
||
init(autoDismissNanoseconds: UInt64 = 2_200_000_000) {
|
||
self.autoDismissNanoseconds = autoDismissNanoseconds
|
||
}
|
||
|
||
/// 显示一条全局 Toast 文案。
|
||
func show(_ message: String) {
|
||
displayToken &+= 1
|
||
self.message = message
|
||
scheduleAutoDismiss(token: displayToken)
|
||
}
|
||
|
||
/// 清除当前 Toast 文案。
|
||
func dismiss() {
|
||
displayToken &+= 1
|
||
dismissTask?.cancel()
|
||
dismissTask = nil
|
||
message = nil
|
||
}
|
||
|
||
/// 将全局 Toast 展示层挂载到指定窗口,通常只在应用根部调用一次。
|
||
func attachToWindow(_ window: UIWindow) {
|
||
if let overlayView, overlayView.superview === window {
|
||
return
|
||
}
|
||
|
||
let overlay = ToastOverlayView(center: self)
|
||
overlay.frame = window.bounds
|
||
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||
window.addSubview(overlay)
|
||
overlayView = overlay
|
||
}
|
||
|
||
/// 为当前 Toast 安排自动隐藏任务,并避免旧任务误清除新 Toast。
|
||
private func scheduleAutoDismiss(token: UInt64) {
|
||
dismissTask?.cancel()
|
||
let delay = autoDismissNanoseconds
|
||
dismissTask = Task { [weak self] in
|
||
do {
|
||
try await Task.sleep(nanoseconds: delay)
|
||
} catch {
|
||
return
|
||
}
|
||
await MainActor.run {
|
||
guard let self, self.displayToken == token else { return }
|
||
self.message = nil
|
||
self.dismissTask = nil
|
||
}
|
||
}
|
||
}
|
||
|
||
#if DEBUG
|
||
/// 测试专用快照,业务代码不应读取 Toast 展示状态。
|
||
var snapshotForTests: ToastSnapshot {
|
||
ToastSnapshot(message: message)
|
||
}
|
||
#endif
|
||
}
|
||
|
||
#if DEBUG
|
||
/// 全局 Toast 测试快照,用于验证文案和自动隐藏行为。
|
||
struct ToastSnapshot: Equatable {
|
||
let message: String?
|
||
}
|
||
#endif
|
||
|
||
/// 全局 Toast 展示层,负责把 Toast 展示到窗口顶部。
|
||
final class ToastOverlayView: UIView {
|
||
private weak var toastCenter: ToastCenter?
|
||
private let bannerView = UIView()
|
||
private let messageLabel = UILabel()
|
||
|
||
/// 使用 Toast 状态中心初始化展示层。
|
||
init(center: ToastCenter) {
|
||
self.toastCenter = center
|
||
super.init(frame: .zero)
|
||
isUserInteractionEnabled = false
|
||
configureViews()
|
||
center.onChange = { [weak self] in
|
||
self?.refreshPresentation(animated: true)
|
||
}
|
||
refreshPresentation(animated: false)
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
nil
|
||
}
|
||
|
||
/// 配置Views展示内容。
|
||
private func configureViews() {
|
||
bannerView.backgroundColor = AppDesign.primary
|
||
bannerView.isHidden = true
|
||
bannerView.alpha = 0
|
||
|
||
messageLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||
messageLabel.textColor = .white
|
||
messageLabel.textAlignment = .center
|
||
messageLabel.numberOfLines = 2
|
||
|
||
bannerView.addSubview(messageLabel)
|
||
addSubview(bannerView)
|
||
|
||
bannerView.translatesAutoresizingMaskIntoConstraints = false
|
||
messageLabel.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
NSLayoutConstraint.activate([
|
||
bannerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||
bannerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||
bannerView.topAnchor.constraint(equalTo: topAnchor),
|
||
|
||
messageLabel.leadingAnchor.constraint(equalTo: bannerView.leadingAnchor, constant: AppMetrics.Spacing.pageHorizontal),
|
||
messageLabel.trailingAnchor.constraint(equalTo: bannerView.trailingAnchor, constant: -AppMetrics.Spacing.pageHorizontal),
|
||
messageLabel.topAnchor.constraint(equalTo: bannerView.safeAreaLayoutGuide.topAnchor, constant: AppMetrics.Spacing.small),
|
||
messageLabel.bottomAnchor.constraint(equalTo: bannerView.bottomAnchor, constant: -AppMetrics.Spacing.small)
|
||
])
|
||
}
|
||
|
||
/// 刷新Presentation展示。
|
||
private func refreshPresentation(animated: Bool) {
|
||
guard let message = toastCenter?.message, !message.isEmpty else {
|
||
hideBanner(animated: animated)
|
||
return
|
||
}
|
||
|
||
messageLabel.text = message
|
||
showBanner(animated: animated)
|
||
}
|
||
|
||
/// 展示Banner。
|
||
private func showBanner(animated: Bool) {
|
||
guard bannerView.isHidden || bannerView.alpha < 1 else { return }
|
||
|
||
bannerView.isHidden = false
|
||
guard animated else {
|
||
bannerView.alpha = 1
|
||
bannerView.transform = .identity
|
||
return
|
||
}
|
||
|
||
bannerView.alpha = 0
|
||
bannerView.transform = CGAffineTransform(translationX: 0, y: -12)
|
||
UIView.animate(withDuration: 0.18, delay: 0, options: [.curveEaseInOut]) {
|
||
self.bannerView.alpha = 1
|
||
self.bannerView.transform = .identity
|
||
}
|
||
}
|
||
|
||
/// 隐藏Banner。
|
||
private func hideBanner(animated: Bool) {
|
||
guard !bannerView.isHidden else { return }
|
||
|
||
guard animated else {
|
||
bannerView.isHidden = true
|
||
bannerView.alpha = 0
|
||
return
|
||
}
|
||
|
||
UIView.animate(withDuration: 0.18, delay: 0, options: [.curveEaseInOut], animations: {
|
||
self.bannerView.alpha = 0
|
||
self.bannerView.transform = CGAffineTransform(translationX: 0, y: -12)
|
||
}, completion: { _ in
|
||
self.bannerView.isHidden = true
|
||
self.bannerView.transform = .identity
|
||
})
|
||
}
|
||
}
|