portico/Sources/Portico/Presentation/ToastManager.swift

82 lines
2.9 KiB
Swift

import Adw
/// Adds and dismisses toasts on a mounted ``ToastOverlay``.
///
/// Create one, attach it with ``WidgetView/toastManager(_:)``, then call
/// ``addToast(_:)`` and ``dismissAll()``:
///
/// ```swift
/// private let toasts = ToastManager()
///
/// var body: some View {
/// ToastOverlay {
/// Button().label("Save").onClicked {
/// toasts.addToast(Toast(title: "Saved", buttonLabel: "Undo") { undo() })
/// }
/// }
/// .toastManager(toasts)
/// }
/// ```
///
/// Toasts added before the overlay mounts are queued and flushed on attach, so
/// a manager is usable from `init` or from a `.task` that races the first mount.
///
/// The manager holds its overlay strongly. Portico does not retain the Swift
/// wrapper objects for widgets it mounts into a container, so a weak reference
/// would be cleared as soon as the mount returned. There is no retain cycle: the
/// overlay does not reference the manager.
@MainActor public final class ToastManager {
private var overlay: Adw.ToastOverlay?
private var pending: [Adw.Toast] = []
/// The attached overlay, or `nil` before mount and after teardown.
@_spi(Portico) public var _attachedOverlay: Adw.ToastOverlay? { overlay }
/// How many toasts are queued awaiting attachment.
@_spi(Portico) public var _pendingCount: Int { pending.count }
/// Creates a manager that is not yet attached to an overlay.
public init() {}
/// Shows `toast` on the attached overlay, queueing it if none is attached yet.
///
/// - Parameter toast: The toast to display. Ownership follows libadwaita's
/// `(transfer full)` contract for `adw_toast_overlay_add_toast`; the caller
/// may release its reference immediately.
public func addToast(_ toast: Adw.Toast) {
guard let overlay else {
pending.append(toast)
return
}
overlay.addToast(toast: toast)
}
/// Dismisses every toast shown or queued on the attached overlay, and
/// discards any toasts queued here before attachment.
public func dismissAll() {
pending.removeAll()
overlay?.dismissAll()
}
/// Binds this manager to `overlay` and flushes the queue.
///
/// The most recent attachment wins.
///
/// - Parameter overlay: The freshly mounted overlay to publish toasts to.
@_spi(Portico) public func _attach(_ overlay: Adw.ToastOverlay) {
self.overlay = overlay
let queued = pending
pending.removeAll()
for toast in queued { overlay.addToast(toast: toast) }
}
/// Releases `overlay` if it is still the attached one.
///
/// Guarding on identity stops an older overlay's teardown from detaching a
/// newer one that has already attached.
///
/// - Parameter overlay: The overlay being torn down.
@_spi(Portico) public func _detach(_ overlay: Adw.ToastOverlay) {
if self.overlay === overlay { self.overlay = nil }
}
}