A toast is a transient, relatively unobtrusive visual component that can be used to display short messages such as status updates or surface errors without blocking user interaction with the main content.
SwiftToasts is a library for SwiftUI that enables easy, fast, flexible and configurable integration of toasts in Apple platforms, at the scene level. Built to follow the API conventions of SwiftUI, using the library feels familiar, intuitive and truly native.
Features:
🎨 Configurable Toast style.
⚙️ Configurable Toast alignment.
🎞️ Configurable Toast transition animations.
⏲️ Deterministic scheduler based Toast presentation.
🍎 Compatible with multiple Apple platforms and all SwiftUI versions.
🛠️ Variety of ways that can be used to present Toasts using SwiftUI inspired APIs.
The SwiftToasts library is compatible with all versions of SwiftUI.
Platform | Compatibility | Tested |
---|---|---|
iOS | ✅ | Device / Simulator |
macOS | ✅ | Device / Simulator |
tvOS | ✅ | Simulator |
watchOS | Device / Simulator | |
visionOS | ✅ | Simulator |
Because watchOS does not have a platform-native dynamic view hierarchy framework such as UIKit or AppKit, SwiftToasts requires that the toastPresentingLayout
modifier be placed at the root content view to enable Toast Presentation on watchOS in a compatibility mode.
You can install SwiftToasts as a Swift package dependency, by using the following url:
https://github.com/athankefalas/swift-toasts.git
A Toast
is defined a plain SwiftUI View and requires three properties to configure and create it, the role of the Toast, the duration and the displayed content view. Similar to common SwiftUI components, such as Button
or Label
, a number of initializers exist that allow the initialization of a Toast with commonly used content.
/// Creating a plain Toast with Text content.
Toast("Hello Toast!")
/// Creating a failure Toast with Text content.
Toast(
"Something went wrong.",
role: .failure
)
/// Creating a success Toast with Label content.
Toast(
"Settings Saved",
systemImage: "checkmark.circle",
role: .success
)
/// Creating a warning Toast with Label and subtitle content.
Toast(
"Network Offline",
value: "Please check your connection.",
systemImage: "network.slash",
role: .warning
)
/// Creating an informational Toast with custom content and long duration.
Toast(role: .informational, duration: .long) {
Label {
Text("User **@\(userName)** sent you a message.")
} icon: {
AsyncImage(
url: URL(string: avatarRawURL)
) { phase in
switch phase {
case .empty, .failure:
EmptyView()
case .success(let image):
image.resizable()
.aspectRatio(contentMode: .fill)
@unknown default:
EmptyView()
}
}
.frame(width: 48, height: 48)
.clipShape(Circle())
}
}
/// Creating an informational Toast with custom content and an custom duration.
Toast(role: .informational, duration: .seconds(8)) {
Label {
Text("Synchronizing data...")
} icon: {
ProgressView()
}
}
The role property of a Toast
defines the semantic purpose for the displayed content and can be used to conditionally modify the appearance of a Toast based on it's role. The role of a Toast is defined using the ToastRole
enum.
The following roles are supported:
-
Plain
A role that presents a plain message with no specific purpose.
-
Informational
A role that presents an informational message to the user, such as a status update or an external event.
-
Success
A role that presents an success message to the user after a user initiated operation was completed.
-
Warning
A role that presents a warning message to the user after a user initiated operation was completed but encountered a recoverable error or a system precept has changed.
-
Failure
A role that presents a failure message to the user after a user initiated operation has failed.
The duration of a Toast
defines how long a Toast presentation will remain active. The duration of a Toast can be defined by using the ToastDuration
type.
Other than predefined defined duration instances that have a specific lifetime, a Toast may also be presented indefinitely, by using the ToastDuration.indefinite
duration. Please note, that a Toast that is presented indefinitely must be explicitly dismissed either by user interaction or by any other means that control the presentation of a Toast.
After a Toast
is created it can be scheduled for presentation on a separate, modal environment. An internal scheduler places scheduled toasts in a FIFO queue and ensures that Toasts will be presented one at a time in the order they were scheduled.
The modifiers below can be used to present a Toast
as a reaction to a trigger, an occurred event or a state change.
The toast
modifier and it's variants can be used to present a Toast
as a reaction to a trigger, an occurred event or a state change. Generally, the toast modifier allows for the optional configuration of the presentation alignment, an optional dismissal callback and a content builder closure that can be used to build the toast to present.
The content builder closure supports conditional and optional Toast
building following the API style of the SwiftUI ViewBuilder.
// Showing a Toast based on a boolean binding.
content
.toast(
isPresented: $showToast,
alignment: .top,
onDismiss: { print("Toast Dismissed.") }
) {
if error != nil {
Toast("Done.", role: .success)
}
}
// Showing a Toast based on an item binding.
content
.toast(
isPresented: $toastItem,
alignment: .top,
onDismiss: { print("Toast Dismissed.") }
) { item in
switch item {
case .completed:
Toast("Completed.", role: .success)
case .failed:
Toast("Failed.", role: .failure)
}
}
// Showing a Toast when some value changes
content
.toast(trigger: someValue) {
Toast("Value changed")
}
// Showing a Toast when some value changes
content
.toast(
trigger: someValue,
alignment: .top,
onDismiss: { print("Toast Dismissed.") }
) { newValue
Toast("Value changed to \(newValue).")
}
// Showing a Toast when a publisher sends a new value.
content
.toast(byReceiving: publisher) { newValue in
Toast("Publisher sent value \(newValue).")
}
The toast variants of the task
modifier can be used to schedule the presentation of a Toast
when the presentation is a direct result of an asynchronous operation.
// Showing a Toast as a result of a task.
content
.task { schedule in
let didSucceed = await operation()
guard !didSucceed else {
return
}
schedule(
toast: Toast(
"Operation failed.",
role: .failure
)
)
}
// Showing a Toast as a result of an identified task.
content
.task(id: identity) { schedule in
let didSucceed = await operation()
guard !didSucceed else {
return
}
schedule(
toast: Toast(
"Operation failed.",
role: .failure
)
)
}
A ToastButton
can be used to schedule the presentation of a Toast
when the presentation is a direct result of a user interaction or user triggered operation.
// Showing a Toast after a user presses a Button.
ToastButton("Submit") { schedule in
operation()
schedule(
toast: Toast(
"Completed.",
systemImage: "checkmark.circle.fill",
role: .success
)
)
}
Most of the aspects of a Toast or it's presentation can be configured using several environment based modifiers.
The style of a Toast can be configured in the same way as some of the system provided components. The library ships with a basic style called PlainToastStyle
, but if further customization is required a custom style can easily be created by conforming to the ToastStyle
protocol.
// Showing a Toast with the default style.
ToastButton("Show Toast") { schedule in
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastStyle(.plain)
// Showing a Toast with a custom style.
ToastButton("Show Toast") { schedule in
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastStyle(SomeToastStyle())
The transition animation when presenting a Toast is also configurable with a selection of predefined transitions. A transition can be combined with another to create a variety of different effects.
// Showing a Toast by fading it in and out.
ToastButton("Show Toast") { schedule in
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastTransition(.opacity)
// Showing a Toast by scaling it in and out.
ToastButton("Show Toast") { schedule in
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastTransition(.scale)
// Showing a Toast by scaling it in and fading it out.
ToastButton("Show Toast") { schedule in
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastTransition(
.asymmetric(
insertion: .scale,
removal: .opacity
)
)
// Showing a Toast by combining two or more transitions.
ToastButton("Show Toast") { schedule in
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastTransition(
.move(edge: .top)
.combined(
with: .opacity.combined(
with: .scale
)
)
)
After a Toast
is created, it is scheduled for presentation in a queue. A scheduled Toast may be cancelled before it is presented by the source it was scheduled from, depending on context and the active environment configuration.
By default, a scheduled toast will not be cancelled unless the scene containing it's source is dismissed. The cancellation policy in the current environment can be configured by using the toastCancellation
modifier.
Please note, that cancellation only affects Toasts that have not yet been presented and are still waiting for presentation in the schedulers queue.
For example, when firing a form submission action using a ToastButton
it might be desirable to save the updated values of the form and immediately dismiss the scene. In order for the scheduled presentation to not be cancelled, the .never
cancellation policy will be required.
// The button below saves the form, starts the dismissal of the active scene
// and then schedules a Toast. By using the `.never` cancellation policy the
// scheduled Tost will not be cancelled when the active scene is dismissed.
ToastButton("Submit") { schedule in
saveForm()
dismiss()
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastCancellation(.never)
Alternatively, when a toast is scheduled by using a state change trigger it might be desirable to avoid scheduling a large number of toasts when a value changes rapidly and frequently. In order to automatically cancel all scheduled toasts by a specific source, the .always
cancellation policy is required.
// A change of the volume value triggers a toast.
// By using the `.always` cancellation policy, each time a new Toast
// is scheduled all previous Toasts already in the scheduler queue,
// will be cancelled.
Slider(value: $volume, in: 0...100) {
Text("Volume: \(volume)%")
}
.toast(trigger: volume) { newValue in
Toast("Volume set to \(newValue)%.")
}
.toastCancellation(.always)
While toasts are usually a fire and forget component, there is a limited capability to dismiss already presented toasts. The toast
modifier and it's variants specifically, use a trigger value as context to determine when to schedule a Toast
. By default, whenever the value changes again an already displayed toast is automatically dismissed as the context that triggered it has changed. This behavior, can be easily configured by using the toastPresentationInvalidation
modifier.
For example, it might be desirable to configure a toast triggered by a value change to automatically dismiss when the value changes and when the source's scene is dismissed.
// A change of the volume value triggers a toast.
// By using the `.contextChanged, .presentationDismissed` presentation
// invalidation options, an already presented Toast will be dismissed when
// the slider's value changes and when it's container is dismissed.
Slider(value: $volume, in: 0...100) {
Text("Volume: \(volume)%")
}
.toast(trigger: volume) { newValue in
Toast("Volume set to \(newValue)%.")
}
.toastCancellation(.always)
.toastPresentationInvalidation([.contextChanged, .presentationDismissed])
Alternatively, if it is desired that the active toast presentation is never invalidated the .never
presentation invalidation can be used instead.
A presented Toast
may be dismissed before it's duration has elapsed as a result of a user tapping the content of the toast. This behavior can be controlled by using the toastInteractiveDismissEnabled
modifier. A common use case to prevent interactive dismissal, is for using a toast as a loading indicator.
// Showing a Toast as a loading indicator HUD.
content
.toast(
isPresented: $isLoading,
alignment: .center
) {
Toast(role: .informational, duration: .indefinite) {
Label {
Text("Loading...")
} icon: {
ProgressView()
.scaleEffect(2)
}
}
}
.toastInteractiveDismissDisabled(true)
When a Toast
is presented it's appearance is retrieved by the source's environment. A custom style can be implemented by creating a struct that conforms to the ToastStyle
protocol.
By using the configuration
parameter and leveraging several environment values, a custom toast style can provide a pretty detailed and adaptive visual representation of the contents of a toast.
import SwiftUI
import SwiftToasts
// A simple example of a custom Toast style
struct CustomToastStyle: ToastStyle {
func makeBody(configuration: Configuration) -> some View {
StyledToastBody(configuration: configuration)
}
struct StyledToastBody: View {
@Environment(\.toastDismiss)
private var toastDismiss
@Environment(\.toastPresentedAlignment)
private var toastPresentedAlignment
@Environment(\.toastInteractiveDismissEnabled)
private var toastInteractiveDismissEnabled
let configuration: Configuration
private var shape: AnyShape {
if toastPresentedAlignment == .center {
return AnyShape(RoundedRectangle(cornerRadius: 12))
} else {
return AnyShape(Capsule())
}
}
private var color: Color {
configuration.role == .failure ? .red : .accentColor
}
var body: some View {
configuration.content
.padding(12)
.foregroundStyle(color)
.background(.ultraThinMaterial, in: shape)
.overlay {
shape.stroke(
color.opacity(0.25),
lineWidth: 1
)
}
.onTapGesture {
guard toastInteractiveDismissEnabled else { return }
toastDismiss?()
}
}
}
}
// Showing a Toast with the custom style.
ToastButton("Show Toast") { schedule in
schedule(
toast: Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
)
}
.toastStyle(CustomToastStyle())
A set of different environment values are injected into a presented toast for the purpose of enabling further customization of the visual content of a Toast
or providing a programmatic dismissal action.
The toast dismiss action is an environment value injected in the toastDismiss
KeyPath and contains an action that can be used to programmatically dismiss a toast depending on a specific user interaction.
Please note, that the scheduler automatically handles the duration of a toast so there is no need for a custom toast style to handle automatic dismissal based on the duration of a presented Toast
.
The toast presented alignment is an environment value injected in the toastPresentedAlignment
KeyPath and contains the alignment of a presented toast. This can be used to modify the appearance of a toast in specific alignments. For example, when a Toast
is presented at the center alignment it might be preferable to use larger font and icon sizes.
The toast interactive dismiss enabled flag is an environment value injected in the toastInteractiveDismissEnabled
KeyPath and controls whether a toast should be dismissed as a result of a user interaction. For example, when implementing a custom toast style this flag could be checked before dismissing a toast when it is tapped.
By default, a Toast is presented in a separate, modal environment on the global scene context. If it is desired for a Toast
to be presented as an overlay over a specific context such as showing a toast inside the context of a sheet presented at the medium
presentation detent, the toastPresentingLayout
modifier can be used.
// Present a sheet at the medium detent and show a Toast inside
content
.sheet(isPresented: $showSheet) {
VStack {
Spacer()
Text("Sheet Content")
Spacer()
Button("Show Toast") {
showToast = true
}
}
.toast(isPresented: $showToast) {
Toast(
"Hello!",
systemImage: "hand.wave.fill",
role: .informational
)
}
.toastPresentingLayout()
.presentationDetents([.medium])
}
Furthermore, due to platform related limitations, on watchOS this modifier is required to present a toast. In general, it is recommended that the view modified using the toastPresentingLayout
modifier be as close as possible to the top level of the target view hierarchy.