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 Color.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)) } }