SwiftUI Confirmation Dialogs

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.

Alert showing something went wrong, try again later and blue OK button

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:

Two buttons arranged vertically and presented from the bottom of the iPhone screen. Top button shows delete all items question in red, bottom button shows cancel in blue.

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.

Popover below a delete button showing delete all items?

On macOS, the dialog appears as a modal alert style dialog with an added Cancel button and, by default, shows the App icon:

macOS confirmation dialog with App icon, delete all items button above default cancel button

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

Dialog title is 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) { ... }
}

First action is text only label showing tell me more…

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.

macOS dialog with trash icon

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.

macOS dialog with don’t ask again checkbox

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:

macOS dialog with exclamation mark in yellow triangle and smaller trash icon