Files
suixinkan_ios_uikit/suixinkan_ios/App/State/ToastCenter.swift
2026-06-26 14:33:31 +08:00

183 lines
5.8 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
}
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)
])
}
private func refreshPresentation(animated: Bool) {
guard let message = toastCenter?.message, !message.isEmpty else {
hideBanner(animated: animated)
return
}
messageLabel.text = message
showBanner(animated: animated)
}
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
}
}
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
})
}
}