Categories
iOS Development SwiftUI

How to build SwiftUI Toast messages or alerts

When talking about toast messages or alerts I do have an alert message in mind that does show a message and usually dissappears by itself. This means, it is different to a native iOS alert where the user has to perform some action to make it disappear. You can see a lot of examples for toast messages on Dribble (see https://dribbble.com/tags/toast-message). On Android there is a native component for this, on iOS there is not. But often enough a designer wants to have toasts on iOS as well. So, we have to build it ourselves! Let’s do that and let’s use SwiftUI for this.

Requirements

Let’s keep it really simple for now. For not getting lost in details, here is a list of our rather rough requirements:

  • Written in SwiftUI
  • Our toast should show a title and a subtitle
    • Some rounded borders to make it look nice
  • The toast should appear from the bottom in an animated way, should stay there for some time and disappear again
  • Three different types of alerts
    • Success: Showing a green checkmark icon
    • Information: Showing a blue info icon
    • Error: Showing a red exclamation mark icon
    • We can use native SFSymbols for the icons (see https://developer.apple.com/sf-symbols/)
  • Ideally: Be able to show multiple toast at the same time and animate them in the order they appeared.

Style the toast

Let’s begin with defining our toast’s message styles. This can be a simple enum with some computed properties for the dynamic properties like the icon and its color.

/// All possible types of toast messages.
enum ToastMessageStyle {

	// A success message
	case success
	// An info message
	case info
	// An error message
	case error

	/// The color associated with the type of message.
	private var color: Color {
		switch self {
		case .success: .green
		case .info: .blue
		case .error: .red
		}
	}

	/// The SFSymbol icon name associated with the type of message.
	private var iconName: String {
		switch self {
		case .success: "checkmark.circle"
		case .info: "info.circle"
		case .error: "exclamationmark.triangle"
		}
	}

	/// The icon view to show.
	var icon: some View {
		Image(systemName: iconName)
		.foregroundStyle(color)
	}
}Code language: Swift (swift)

As we style the icon already within the ToastMessageStyle, we can make everything else private. So, the only property we actually need to access from the outside for is icon. If you want to do something else with the color property, e.g. draw colored borders then feel free of course to make this non-private as well.

Toast Message View Model

Now, let’s add the view model for describing a toast message. This should have a title, a message and the icon. The icon can be derived from the message’s style we just created via its icon property. So let’s add a style property as well.

We also make this view model Identifiable and also Equatable as we need to be able to keep track of the messages somehow.

/// Toast message view model
struct ToastMessage: Identifiable, Equatable {

	/// The toast message's identifier.
	let id: String

	/// The title
	let title: String

	/// The message
	let message: String

	/// The style of the toast message
	let style: ToastMessageStyle
}Code language: Swift (swift)

Toast Message SwiftUI View

Now we can create a simple view that does use the just created view model (ToastMessage). Let’s use a simple layout:

  • On the leading edge, there should be the icon
  • On the trailing edge there should be the title and message
    • Title is at the top with some more prominent fontstyle
    • Message is at the bottom with some more subtle fontstyle
  • There should be some background so we can have a nice box with rounded corners

Here is what I came up with then given these simple design requirements above:

/// A view showing a toast message.
struct ToastMessageView: View {

	let viewModel: ToastMessage

	var body: some View {
		HStack(alignment: .top) {
			viewModel.style.icon
			VStack(alignment: .leading) {
				Text(viewModel.title)
					.font(.callout)
				Text(viewModel.message)
					.font(.caption)
					.foregroundStyle(.secondary)
			}
		}
		.padding(8)
		.background {
			Color.gray.opacity(0.1)
		}
		.clipShape(.rect(cornerRadius: 4))
    }
}Code language: Swift (swift)

I added some padding so that the box has a little more breathing space, also some very subtle background color and also rounded borders as promised. This is how the toast messages do look now when rendering in a #Preview block with each of the styles:

Toast views created with SwiftUI
All of the toast message styles created with SwiftUI

Nice, I think it’s looking good enough for now. We can make it nicer later if we want to but let’s keep it like that for now. If we want to add buttons or something like that we can do that as well later.

Make the toast pop!

Alright, now we have the toast message view and some models for it. We can already show it on screen but of course what makes a toast a toast message is its animation. Toast messages come from the bottom and also disappear towards the bottom. Similar to what a bread does in your toaster.

In SwiftUI we can just chain a couple of animation modifiers together to achieve this. There are probably also other ways to do this but for me it worked quite nicely by just chaining them like this (add this after the clipShape modifier of the view):

		.transition(
			.asymmetric(
				insertion: .push(from: .bottom),
				removal: .push(from: .top)
			)
			.combined(with: .opacity)
		)Code language: Swift (swift)

What we will quickly see when playing around with the animations is that we need an “asymmetric” animation. As we want to move in from the bottom and move out to the bottom. You can use SwiftUI’s .asymmetric modifier for setting two different animations for insertion and removal of the view. Finally, as we do not want to see the view suddenly disappearing we combine it with an .opacity animation.

If you wrap your view now inside of an if block and toggling it via a Button you can already see the toast animation:

Button("Show Toast") {
	withAnimation {
		isShowingToast.toggle()
	}
}Code language: Swift (swift)
Our toast animation so far

On simple screens where we only ever want to see a single toast message, this might already be enough. However, if we want to manage multiple toast messages and their appearance we need to add some container for keeping track of all of them.

Introducing Toast Message Container

The simple idea is to have a view that show toast messages as they “come in” and let’s them disappear in the order they came in. This means we want to create a first in, first out (FIFO) logic. Views should appear and disappear in an animated manner as they are added to an array.

Here is the view just doing that:

/// Container managing `ToastMessageView`s in a first-in, first-out manner using animations.
struct ToastMessageContainer: View {

	/// All the toast messages to be shown.
	@Binding var toastMessages: [ToastMessage]

	/// Currently shown toast messages.
	@State var visibleMessages: [ToastMessage] = []

    var body: some View {
		VStack {
			ForEach(visibleMessages) { model in
				ToastMessageView(viewModel: model)
					.onAppear {
						showNextMessage()
						DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
							withAnimation {
								visibleMessages.removeAll { $0 == model }
							}
						}
					}
			}
		}
		.onChange(of: toastMessages, { oldValue, newValue in
			if newValue.count > oldValue.count {
				showNextMessage()
			}
		})
    }

	/// Shows next message from message stack.
	func showNextMessage() {
		withAnimation(Animation.bouncy(extraBounce: 0.2)) {
			if let lastMessage = toastMessages.popLast() {
				self.visibleMessages.append(lastMessage)
			}
		}
	}
}Code language: PHP (php)

By having a Binding we can manage the toast messages from the outside. Whenever a message is added, this message will be shown and its disappearance managed by the ToastMessageContainer.

Wrapping it up

Now we can add our ToastMessageContainer to some view. For testing purposes I just added three buttons for adding toast messages to the array of ToastMessage.

struct ToastScreen: View {

	@State var messages: [ToastMessage] = []

	var body: some View {
		VStack {
			Spacer()
			HStack {
				Group {
					Button("show success") {
						messages.append(
							ToastMessage(
								id: UUID().uuidString,
								title: "Hello",
								message: "Success Message",
								style: .success
							)
						)
					}
					Button("show info") {
						messages.append(
							ToastMessage(
								id: UUID().uuidString,
								title: "Hello",
								message: "Info Message",
								style: .info
							)
						)
					}
					Button("show error") {
						messages.append(
							ToastMessage(
								id: UUID().uuidString,
								title: "Hello",
								message: "Error Message",
								style: .error
							)
						)
					}
				}
				.buttonStyle(.bordered)
			}
			Spacer()
		}
		.overlay(alignment: .bottom) {
			ToastMessageContainer(toastMessages: $messages)
		}
	}
}Code language: PHP (php)

And this is how it looks in action:

Conclusion

As you saw it is pretty straight forward to add simple toast views to your app using SwiftUI. Of course this solution still has some room for improvement (e.g. customizable timings or interactivity) but it should be a great start to iterate from.

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Overview
mic.st

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.

Strictly Necessary Cookies

Strictly Necessary Cookie should be enabled at all times so that we can save your preferences for cookie settings.