Howl is version 3 of the BezelNotification package. Original functionality will be preserved, but future focus will be on in-window toasts. You can still
import BezelNotification, but you're encouraged toimport Howlinstead.
Formerly (BH)BezelNotification, Howl is a way to present toasts in your apps in Apple platforms.
Maybe you already know what it is, but it's not a HIG paradigm so I'll explain here for those who don't know.
Toasts are brief messages that appear on-screen for a moment, to tell the user that something happened, and then go away. They're a very common paradigm in Android, and Apple system-level things sometimes use them as well, though folks have historically called these things like "bezel notifications" "popup UI", etc.. Things like the volume UI coming up when you change the volume, or Xcode's "Build Succeeded", or the Apple Pencil charging UI when you place it on the side of your iPad.
Here's some examples from outside this library:
↖️ Snackbar example from Material Design M3 guidelines↗️ Android toast example from Android Developers toast documentation↙️ macOS bezel notification from Xcode 13 on macOS 12↘️ Apple Pencil charging notification from iPadOS 18
This is designed to strike a balance between ease-of-use and customizability. For instance, this is the primary way it is intended to be used in the general case:
myView
.toast(isPresented: $showLinkCopiedToast, text: "Link copied")You can also specify an icon, a duration, and a call-to-action. Different toast styles may choose to omit icons andor calls-to-action.
myView
.toast(
// These are required fields:
isPresented: $showDraftSavedToast,
text: "Message saved to draft",
// These are optional fields:
duration: .importantText,
icon: Image(systemName: "tray.and.arrow.down"),
action: .init(label: "Show drafts") {
openDrafts()
}
)All toasts let you say how long they show. There are currently three options:
actionFeedbackis for toasts which are only shown for a brief moment to confirm that an action occurred. These toasts only remain on-screen long enough to allow the user to read a few words.importantTextis for toasts which explain something important to the usermanualDismissleaves the toast on-screen forever. The dev can dismiss them arbitrarily, and toasts which allow the user to dismiss them can be dismissed by the user.
ℹ️ Aside from
manualDismiss, the durations above aren't tied to actual time intervals. Avoid assuming that any one of these means any specific amount of time. The actual time interval that a toast is shown on-screen varies depending on factors like whether or not it's presenting a call-to-action, and might also change across versions.
myView
.toast(isPresented: $showDownloadCompleteToast, text: "Download complete", duration: .actionFeedback)myView
.toast(
isPresented: $showErrorToast,
text: """
Could not save the file:
\(error.localizedDescription)
""",
duration: .importantText)myView
.toast(isPresented: $isLoggedOut, text: "You've been logged out", duration: .manualDismiss)You can specify any image as an icon for toasts which support them.
myView
.toast(isPresented: $showBuildSucceededToast, text: "Build succeeded", icon: Image(systemName: "hammer.fill"))myView
.toast(isPresented: $showCelebrationToast, text: "Happy \(birthdayOrdinal) birthday!", icon: Image("party"))Some toasts support a "call-to-action", which is a control (e.g. a button) which offers the user a simple action they can take, related to the toast.
For example, if the toast tells the user that a new item has been created in the background, it might offer a "View" call-to-action which brings that item to the foreground.
myView
.toast(
isPresented: $showItemCreatedToast,
text: "Item created",
action: .init(label: "View") {
navigate(to: newItem)
}
)You can further customize the appearance using toast styles.
myView
.toast(isPresented: $isLoading, text: "Link copied")
.toastStyle(.snackbar)This, just like all SwiftUI styles, can be applied to parent views and will cascade to child views.
For example, you can set this on your ContentView to set the style of all toasts in your app, but have one which is different by setting the style on its view.
Of course, if you don't specify a style, a reasonable .default will be used instead.
This repo comes with some premade styles to get you started!
You know those square notifications macOS does when you change the volume 'n' stuff? This is like that, but you can actually use it in your projects.
myView
.toastStyle(.systemBezel)
This does not use any secret system APIs (but instead creates its own bezel notifications from scratch), so this cannot interact with nor affect macOS's own system bezel notifications. If one is already showing, this might obscure it or be obscured by it instead of replacing it or waiting for it to hide.
This also means it can be used in App Store apps 🥳
The legacy of this package is its ability to show "bezel notifications" which look exactly like the system bezel notifications.
Version 3 of this package focuses more on in-app toasts, but it does indeed preserve and actively maintain the original functionality, just renamed from BHBezelNotification to SystemBezelNotification and moved from direct usage to the same API as all the new toast styles. Just use .toastStyle(.systemBezel), and the same .toast(... API that all toasts use!
Of course, because of how system bezels work, they're only available on macOS. Other platforms cannot use it, but they can still use the Bezel style, which mimics the same style but constrained within a view.
This is the in-window version of the System Bezel toast, which is available on all supported platforms.
myView
.toastStyle(.bezel)
This is similar to the kind of bottom-left notifications you'd see in many websites and Android apps.
myView
.toastStyle(.snackbar)
This is similar to the kind of bottom-center notifications you'd see in various Android apps and low-priority system alerts.
myView
.toastStyle(.capsule)
Toast styles are open; you can create your own! (Unlike PickerStyle 😤)
All you have to do is create a struct which implements the ToastStyle protocol. You style the way the toast will look when it appears, and this framework handles the rest.
struct MyToastStyle: ToastStyle {
func body(_ configuration: Configuration, environment: EnvironmentValues) -> some View {
Text(configuration.text)
.foregroundStyle(.black)
.background(Color.pink.blendMode(.plusLighter))
.onTapGesture(perform: configuration.action?.userDidInteract ?? {})
.transition(.move(edge: .top).animation(.bouncy))
}
}
extension ToastStyle where Self == MyToastStyle {
static var mine: Self { .init() }
}ℹ️ Be aware that this is not run within the SwiftUI framework. It must build a SwiftUI view in its
body(which will be rendered within SwiftUI), and that body function will be passed the current environment values in case it needs them. If you need to use things like@Stateor@EnvironmentObjectfields, you can use a custom SwiftUI view somewhere inside the view built by thebodyfunction, and inside that custom view you may use@Stateand all other SwiftUI paradigms.
To try out Howl without installing it into your own project first, you can use this demo app I put together!
