Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,182 @@
//
// 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
})
}
}