Files
suixinkan_ios_uikit/suixinkan_ios/App/State/ToastCenter.swift
汉秋 d99a5b1bf8 Advance UIKit rewrite with AMap integration and core UI modules.
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>
2026-06-26 15:16:12 +08:00

187 lines
5.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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