77 lines
2.8 KiB
Swift
77 lines
2.8 KiB
Swift
import SwiftUI
|
|
|
|
struct Toast: Identifiable {
|
|
enum Style {
|
|
case success, error, warning, info, git
|
|
var icon: String {
|
|
switch self {
|
|
case .success: return "checkmark.circle.fill"
|
|
case .error: return "xmark.circle.fill"
|
|
case .warning: return "exclamationmark.triangle.fill"
|
|
case .info: return "info.circle.fill"
|
|
case .git: return "arrow.triangle.branch"
|
|
}
|
|
}
|
|
var color: Color {
|
|
switch self {
|
|
case .success: return .green; case .error: return .red
|
|
case .warning: return .orange; case .info: return .accentColor; case .git: return .purple
|
|
}
|
|
}
|
|
}
|
|
let id = UUID()
|
|
let message: String
|
|
let style: Style
|
|
let duration: TimeInterval
|
|
init(_ message: String, style: Style = .info, duration: TimeInterval = 3) {
|
|
self.message = message; self.style = style; self.duration = duration
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class ToastStore: ObservableObject {
|
|
static let shared = ToastStore()
|
|
@Published var toasts: [Toast] = []
|
|
|
|
func show(_ toast: Toast) {
|
|
toasts.append(toast)
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(toast.duration))
|
|
toasts.removeAll { $0.id == toast.id }
|
|
}
|
|
}
|
|
func show(_ message: String, style: Toast.Style = .info, duration: TimeInterval = 3) {
|
|
show(Toast(message, style: style, duration: duration))
|
|
}
|
|
}
|
|
|
|
struct ToastOverlay: ViewModifier {
|
|
@ObservedObject var store: ToastStore
|
|
func body(content: Content) -> some View {
|
|
content.overlay(alignment: .bottom) {
|
|
VStack(spacing: 8) {
|
|
ForEach(store.toasts) { toast in
|
|
HStack(spacing: 10) {
|
|
Image(systemName: toast.style.icon).foregroundStyle(toast.style.color)
|
|
Text(toast.message).font(.subheadline).lineLimit(2)
|
|
Spacer()
|
|
Button { store.toasts.removeAll { $0.id == toast.id } }
|
|
label: { Image(systemName: "xmark").font(.caption.bold()).foregroundStyle(.secondary) }
|
|
.buttonStyle(.plain)
|
|
}
|
|
.padding(.horizontal, 14).padding(.vertical, 10)
|
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
|
|
.shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4)
|
|
.padding(.horizontal, 16)
|
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
|
}
|
|
}
|
|
.padding(.bottom, 60)
|
|
.animation(.spring(response: 0.3), value: store.toasts.count)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func toastOverlay() -> some View { modifier(ToastOverlay(store: ToastStore.shared)) }
|
|
}
|