Alert Coordinator in SwiftUI with @Observable
Displaying alerts in SwiftUI is easy, but every view that displays alerts has to add the .alert() modifier, which makes the code quite difficult to read at the end of the day. Alerts can be displayed simultaneously but only 1 alert can ever be presented and acted upon by the user at the time. It would be better if we could abstract this away and allow views and view models to display alerts without having to re-implement the .alert() modifier per view/view model.
In this post we will create an alert coordinator with Swift’s @Observable
pattern.
Our Goal
Look at this view model! By the end of this article we will be able to display alerts from anywhere, view, view model, etc, by simply calling a showAlert()
function which we take in using the @ShowAlert
property wrapper. Alerts can be synchronous or asynchronous. If they are synchronous, we will be able to get a call-back when any of the buttons are tapped, like this:
extension ContentView {
final class ViewModel {
@ShowAlert private var showAlert
@MainActor
func displayAlert() {
showAlert(
Alert(
title: "Title",
message: "Message",
actions: [
.init(
title: "Button 1",
onPressed: {
print("Button 1 pressed")
}
),
.init(
title: "Button 2",
onPressed: {
print("Button 2 pressed")
}
),
]
)
)
}
}
}
And in case of displaying asynchronous alerts, we can use another type of alert which we’ll call AsyncAlert
:
extension ContentView {
final class ViewModel {
@ShowAlert private var showAlert
@MainActor
func displayAlert() async -> Int {
await showAlert(
AsyncAlert(
title: "Title",
message: "Message",
actions: [
.init(
title: "Button 1",
value: 10
),
.init(
title: "Button 2",
value: 20
),
]
)
)
}
}
}
Note how the displayAlert()
function in the view model returns an Int
asynchronously! That all works because the showAlert()
function when displaying an AsyncAlert
returns a generic value with which the alert actions are configured with. In this case, an Int
! Don’t worry if this all sounds confusing for now! We will learn about this together.
Synchronous Alert Structure
The basis to all of this is an alert structure. An alert has to have:
- Title
- Message
- Actions (array)
Let’s define this alert structure:
@MainActor
struct Alert {
let title: LocalizedStringKey
let message: LocalizedStringKey
let actions: [Action]?
init(
title: LocalizedStringKey? = nil,
message: LocalizedStringKey? = nil,
actions: [Action]? = nil
) {
self.title = title ?? ""
self.message = message ?? ""
self.actions = actions
}
}
This is enough for us to be able to start implementing the alert coordinator itself. The alert coordinator has to have a published property that the root view of our project can read from, and display an alert based on.
Alert Actions
The Alert
structure should be able to also define Action
as seen above so let’s start with that now:
extension Alert {
struct Action: Identifiable {
let id = UUID()
let title: LocalizedStringKey
let onPressed: () -> Void
let role: ButtonRole?
init(
title: LocalizedStringKey,
role: ButtonRole? = nil,
onPressed: @escaping () -> Void
) {
self.title = title
self.role = role
self.onPressed = onPressed
}
}
}
So that’s for the Action
of each Alert
.
Alert Views
Let’s go ahead and create an extension on Alert
that produces views for the message and all the actions of the Alert
:
extension Alert {
func messageView() -> some View {
Text(message)
}
func actionsView() -> some View {
ForEach(actions ?? []) { action in
Button(role: action.role) {
action.onPressed()
} label: {
Text(action.title)
}
}
}
}