Alerts in SwiftUI
How to present Alerts in SwiftUI efficiently

Photo by Luís Perdigão on Unsplash
I recently published my first app on the app store written in SwiftUI. During development, finding out how to create a single alert was very easy, with lots of resources online. However, once my application scaled up, I found that the common approach was unreliable when the cases of showing an alert grew.
This led me to write this article to go through the different approaches I have used, starting from a very simple alert to a more efficient approach that handles the different alert types, consecutive alerts, and child views presenting an alert.
Creating a Simple Alert
Creating an alert in SwiftUI can be done with a single @State variable of type Boolean and the .alert(isPresented) modifier. Whenever this variable gets set to true, the view will be reloaded and the alert will be presented. Once the alert is dismissed, Swift will handle setting the variable back to false.
struct ContentView: View {
@State var showAlert = false
var body: some View {
Button(action: {
self.showAlert.toggle()
}, label: {
Text("SHOW ALERT")
})
.alert(isPresented: self.$showAlert, content: {
Alert(title: Text("I'm an alert"))
})
}
}Customising the alert
The content closure of the alert has to be of type alert. These can vary in complexity. Alerts, by default, have a title and a dismiss button, but they can be customised to have a message and up to two buttons which can be tied to any functionality.
Some examples below:
struct ContentView: View {
@State var showAlert = false
var body: some View {
Button(action: {
self.showAlert.toggle()
}, label: {
Text("SHOW ALERT")
})
.alert(isPresented: self.$showAlert, content: {
Alert(title: Text("I'm an alert"), message: Text("An alert message"), dismissButton: .default(Text("Bye alert!"), action: {
/// insert action for when user dismisses alert
}))
})
}
}Alert with a custom message and dismiss button.
struct ContentView: View {
@State var showAlert = false
var body: some View {
Button(action: {
self.showAlert.toggle()
}, label: {
Text("SHOW ALERT")
})
.alert(isPresented: self.$showAlert, content: {
Alert(title: Text("I'm an alert"), message: Text("Are you sure you want to do this?"), primaryButton: .default(Text("Yes"), action: {
/// insert action for when user confirms they want to do this action
}), secondaryButton: .cancel())
})
}
}Alert with a custom message and a primary and secondary button.
Why this approach is not scalable
The above approach works well, but in practice, a view may need to show different alerts depending on the user's actions. In SwiftUI, a view can only have a single .alert modifier attached to it.
A possible solution is to apply an alert modifier to each individual view element, but that brings inefficiency with duplicate code and multiple @State variables, which become challenging to keep track of — especially if child views exist which also present alerts.
struct ContentView: View {
@State var showAlertOne = false
@State var showAlertTwo = false
var body: some View {
VStack{
/// button 1
Button(action: {
self.showAlertOne.toggle()
}, label: {
Text("SHOW ALERT 1")
})
.alert(isPresented: self.$showAlertOne, content: {
Alert(title: Text("I'm an alert"))
})
/// button 2
Button(action: {
self.showAlertTwo.toggle()
}, label: {
Text("SHOW ALERT 2")
})
.alert(isPresented: self.$showAlertTwo, content: {
Alert(title: Text("I'm another alert"))
})
}
}
}Introducing AlertItem
A better solution is to make use of the .alert(item) modifier, which binds to an identifiable object and is presented whenever the identifiable item is not nil or is updated.
I like to use a custom struct which is similar to the SwiftUI alert struct and conforms to the Identifiable protocol.
struct AlertItem: Identifiable {
var id = UUID()
var title = Text("")
var message: Text?
var dismissButton: Alert.Button?
var primaryButton: Alert.Button?
var secondaryButton: Alert.Button?
}This allows an alert of varying complexity to be created on the fly when needed and doesn’t require multiple .alert modifiers or @State variables.
struct ContentView: View {
@State var alertItem : AlertItem?
var body: some View {
VStack{
/// button 1
Button(action: {
self.alertItem = AlertItem(title: Text("I'm an alert"), message: Text("Are you sure about this?"), primaryButton: .default(Text("Yes"), action: {
/// insert alert 1 action here
}), secondaryButton: .cancel())
}, label: {
Text("SHOW ALERT 1")
})
/// button 2
Button(action: {
self.alertItem = AlertItem(title: Text("I'm another alert"), dismissButton: .default(Text("OK")))
}, label: {
Text("SHOW ALERT 2")
})
}.alert(item: $alertItem) { alertItem in
guard let primaryButton = alertItem.primaryButton, let secondaryButton = alertItem.secondaryButton else{
return Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
}
return Alert(title: alertItem.title, message: alertItem.message, primaryButton: primaryButton, secondaryButton: secondaryButton)
}
}
}Then if you have child views that also present alerts, you can pass the AlertItem as binding and be confident that the behavior should be no different.
Presenting an Alert Within an Alert
If you want to change the value of an AlertItem within the action of another AlertItem in the hope that it will dismiss the first alert and show the second one… it won’t.
This seems like a niche problem to meet; however, in an application, there may be an alert of the form:
Are you sure you want to <some action>?
If the user then confirms but the action fails, i.e loss of network connection, then it will be helpful to show another alert to say an error has occurred.
The solution is to update the value of the AlertItem asynchronously. Once the action of the first alert has finished, the UI will be updated.
struct ContentView: View {
@State var alertItem : AlertItem?
var body: some View {
/// button
Button(action: {
self.alertItem = AlertItem(title: Text("I'm an alert"), message: Text("Are you sure about this?"), primaryButton: .default(Text("Yes"), action: {
/// trigger second alert
DispatchQueue.main.async {
self.alertItem = AlertItem(title: Text("Error"), message: Text("An unexpected error occurred"), dismissButton: .default(Text("OK")))
}
}), secondaryButton: .cancel())
}, label: {
Text("SHOW ALERT")
}).alert(item: $alertItem) { alertItem in
guard let primaryButton = alertItem.primaryButton, let secondaryButton = alertItem.secondaryButton else{
return Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
}
return Alert(title: alertItem.title, message: alertItem.message, primaryButton: primaryButton, secondaryButton: secondaryButton)
}
}
}If you have any questions, post a comment below.
