In iOS 15, and macOS 12, SwiftUI added a convenient view modifier to present a confirmation dialog to the user.
Last updated: Mar 14, 2024
Sheets, Alerts and Confirmations
SwiftUI provides a generic sheet and a more specific alert for presenting dialog views to the user. The Apple iOS Human Interface Guidelines recommend you use an alert to tell the user something important has happened.
When you need to present choices related to an action the user is taking prefer to use an action sheet. When the action is destructive or undoable present a confirmation dialog to ask the user to confirm or cancel the action.
Alert Views
In iOS 15, you present an alert with a view modifier:
@State private var isPresentingAlert: Bool = false
var body: some View {
Button("Save") {
if !store.save()
isPresentingAlert = true
}
}
.alert("Something went wrong, try again later",
isPresented: $isPresentingAlert) {
}
}
In common with sheets, you use a binding to a boolean which when set to true triggers the system to present the alert. The system presents the alert modally in the center of the screen. If you don’t add any actions the system provides a default “OK” action. All actions dismiss the alert for you.
Confirmation Dialogs
In iOS 15, and macOS 12, SwiftUI added the confirmation dialog view to ask the user for confirmation. For example, if I have a button that will delete all items:
struct DeleteButton: View {
@EnvironmentObject var store: Store
@State private var isPresentingConfirm: Bool = false
var body: some View {
Button("Delete", role: .destructive) {
isPresentingConfirm = true
}
.confirmationDialog("Are you sure?",
isPresented: $isPresentingConfirm) {
Button("Delete all items?", role: .destructive) {
store.deleteAll()
}
}
}
}
I’ve added the .destructive
role to the button which causes the system to highlight it in red:
The confirmation dialog has a couple of advantages over a generic sheet:
- You do not need to add a cancel button. The confirmation dialog includes a standard dismiss action by default.
- All actions in the confirmation dialog dismiss the dialog. As with an alert, you do not need to manually dismiss the dialog by setting the boolean state variable to false.
On an iPhone (and compact size classes) the system shows the dialog as a sheet that slides up from the bottom. On an iPad (regular size classes) it’s shown as a popover without a cancel button. A user can dismiss the dialog, cancelling the action, by tapping outside the dialog.
On macOS, the dialog appears as a modal alert style dialog with an added Cancel button and, by default, shows the App icon:
Adding a Message
You can include a message in the dialog:
.confirmationDialog("Are you sure?",
isPresented: $isPresentingConfirm) {
Button("Delete all items?", role: .destructive) { ...}
} message: {
Text("You cannot undo this action")
}
Text Only Controls
On iOS, tvOS and watchOS the buttons you add to the dialog only support text labels. If you include an image it’s ignored:
.confirmationDialog("Are you sure?",
isPresented: $isPresentingConfirm) {
Button { ...
} label: {
Label("Tell me more...",
systemImage: "questionmark.circle")
}
Button("Delete all?", role: .destructive) { ... }
}
macOS Extras
The confirmation dialog has some extra capabilities for macOS. As I mentioned above, the dialog shows the App icon by default. You can override the icon with the .dialogIcon
modifier (macOS 13+):
.confirmationDialog(...) { ...
}
.dialogIcon(Image(systemName: "trash.circle.fill"))
This also works for watchOS 10+ but is ignored on other platforms.
You can also include a checkbox to allow the user to request that you don’t display the confirmation again (macOS 14+):
struct DeleteButton: View {
@EnvironmentObject var store: Store
@State private var isPresentingConfirm: Bool = false
@AppStorage("SuppressDeleteConfirm")
private var suppressDeleteConfirm = false
var body: some View {
Button("Delete", role: .destructive) {
if !suppressDeleteConfirm {
isPresentingConfirm = true
}
}
.confirmationDialog("Are you sure?",
isPresented: $isPresentingConfirm) {
Button("Delete all items?", role: .destructive) {
store.deleteAll()
}
}
.dialogIcon(Image(systemName: "trash.circle.fill"))
.dialogSuppressionToggle(isSuppressed: $suppressDeleteConfirm)
}
}
This modifier is ignored on other platforms.
Note that you need to do the work to suppress showing the dialog, storing the user’s choice in AppStorage.
Both the .dialogIcon
and the dialogSuppressionToggle
modifiers are available cross-platform, starting with iOS 17, so there’s no need to conditionally compile them for macOS.
One final option which is only available on macOS 13+, watchOS 10+, and visionOS 1.0+, is the ability to change the severity of the dialog:
.confirmationDialog(...) { ...
}
#if os(macOS)
.dialogSeverity(.critical)
#endif
This adds a warning icon to the dialog overlayed with the dialog icon: