Skip to content

BlueHuskyStudios/BezelNotification

Repository files navigation

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 to import Howl instead.

Howl: Toasts for SwiftUI

Swift 5.2+ macOS 14+ iOS 17+ The Fair License

Howl: Toast notifications for SwiftUI

Formerly (BH)BezelNotification, Howl is a way to present toasts in your apps in Apple platforms.

Howl demo on iOS Howl demo on macOS

What is a toast?

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:

Examples of external toast messages

Usage

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()
        }
    )

⏲️ Duration

All toasts let you say how long they show. There are currently three options:

  • actionFeedback is 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.
  • importantText is for toasts which explain something important to the user
  • manualDismiss leaves 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)

♨️ Icons

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"))

🎯 Call to action

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

Styles

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!

System Bezel

macOS 14+

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)
A comparison between this package's bezel notifications and the macOS native ones. They're identical, except this package's version is more customizable.

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.

Bezel

macOS 14+ iOS 17+

This is the in-window version of the System Bezel toast, which is available on all supported platforms.

myView
    .toastStyle(.bezel)
Screenshot of the bezel toast

Snackbar

macOS 14+ iOS 17+

This is similar to the kind of bottom-left notifications you'd see in many websites and Android apps.

myView
    .toastStyle(.snackbar)
Screenshot of the snackbar toast

Capsule

macOS 14+ iOS 17+

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)
Screenshot of the capsule toast

Custom styling

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 @State or @EnvironmentObject fields, you can use a custom SwiftUI view somewhere inside the view built by the body function, and inside that custom view you may use @State and all other SwiftUI paradigms.

Try it out!

To try out Howl without installing it into your own project first, you can use this demo app I put together!