SwiftUI Tutorial

SwiftUI Alert: The Modern .alert Modifier, Roles, and confirmationDialog

Everything you need to present alerts in SwiftUI the current way — the value/isPresented modifiers, item-driven alerts, text-field alerts, button roles, and action sheets via confirmationDialog. Compilable Swift 6 code, real pitfalls, and iOS 26 notes.

Last updated: 2026-07-17 9 min read By Ahmed Gagan, iOS Engineer
Quick Answer

Present alerts in SwiftUI with the view modifier .alert(_:isPresented:) { /* action buttons */ } message: { /* body */ }. Drive it from a Bool @State, or use .alert(_:isPresented:presenting:) / .alert(item:) to carry data. Buttons take a role of .destructive or .cancel, and multi-choice action sheets use .confirmationDialog. The old Alert() struct is deprecated — don't build alerts by returning Alert values anymore.

Minimum iOS
iOS 15+ for the modern .alert(_:isPresented:) closure API
Action sheets
Use .confirmationDialog, not the deprecated .actionSheet
Text fields
Add TextField inside the .alert actions closure (iOS 16+)
Swift Kit support
The Swift Kit ships a typed AppError enum wired to an item-driven alert, so every thrown error surfaces a friendly, localized dialog.

The modern alert modifier

SwiftUI alerts are attached to a view with the .alert modifier rather than constructed as objects. The most common form takes a title, a Bool binding that controls presentation, and two trailing closures — one for the action buttons and one for the message body. When the binding flips to true the system presents the alert; dismissing any button sets it back to false for you, so you rarely toggle it manually on the way out. Keep the title short and put explanatory text in the message closure. If you provide no buttons, SwiftUI supplies a default OK button automatically, but being explicit reads better and lets you attach roles.

A basic titled alert with a message
struct SaveView: View {
    @State private var showConfirmation = false

    var body: some View {
        Button("Save Draft") { showConfirmation = true }
            .alert("Draft Saved", isPresented: $showConfirmation) {
                Button("OK", role: .cancel) { }
            } message: {
                Text("Your draft was stored locally and will sync when you're back online.")
            }
    }
}

Button roles: destructive and cancel

Every alert button can carry a role that changes both its appearance and its placement. A .destructive button is rendered in red and is the correct choice for anything irreversible like deleting an account. A .cancel button is bolded and pinned so the user can always back out; SwiftUI also treats a tap outside or the hardware back gesture as the cancel action. You should include exactly one cancel button in a destructive flow. Note the button order in code does not always match on-screen order — the system positions cancel and destructive buttons according to platform conventions, so don't rely on declaration order for layout.

  • .destructive renders red and signals an irreversible action
  • .cancel is bolded, always dismissible, and should appear once
  • A nil role is a normal default button
  • System reorders roles to match platform HIG — never assume code order equals visual order
A destructive confirmation with a cancel escape hatch
struct DeleteAccountButton: View {
    @State private var confirmDelete = false
    let onDelete: () -> Void

    var body: some View {
        Button("Delete Account", role: .destructive) { confirmDelete = true }
            .alert("Delete your account?", isPresented: $confirmDelete) {
                Button("Delete", role: .destructive) { onDelete() }
                Button("Keep Account", role: .cancel) { }
            } message: {
                Text("This permanently removes your data. This cannot be undone.")
            }
    }
}

Data-driven alerts: presenting: and item:

A raw Bool is fine for a single fixed message, but real apps need the alert to carry data — which row failed, which error was thrown. Two overloads solve this. .alert(_:isPresented:presenting:) hands the non-optional presented value into both closures so you can format a message from it. The item-driven variant, .alert(_:isPresented:presenting:) paired with an optional @State, is ideal for errors: set the optional to a value to present, and SwiftUI clears it on dismiss. This pattern keeps a single alert modifier that adapts to whatever error occurred instead of scattering booleans across your view.

Item-driven error alert with a typed error
struct AppError: Identifiable {
    let id = UUID()
    let title: String
    let message: String
}

struct UploadView: View {
    @State private var activeError: AppError?

    var body: some View {
        Button("Upload") { attemptUpload() }
            .alert(item: $activeError) { error in
                Alert(title: Text(error.title), message: Text(error.message))
            }
    }

    private func attemptUpload() {
        activeError = AppError(title: "Upload Failed",
                               message: "Check your connection and try again.")
    }
}

Text-field alerts and confirmationDialog action sheets

Since iOS 16 you can drop a TextField (or SecureField) directly inside the alert actions closure to collect a short string — a rename, a passcode, a nickname. Bind it to @State and read the value in your confirm button. For presenting several mutually exclusive choices, use .confirmationDialog, the replacement for the deprecated .actionSheet. It renders as a bottom sheet on iPhone and a popover on iPad, supports the same roles, and takes a titleVisibility argument to show or hide the header. Reserve confirmationDialog for 2–4 choices; more than that belongs in a real list or menu.

Rename alert plus a confirmationDialog
struct ItemRow: View {
    @State private var newName = ""
    @State private var showRename = false
    @State private var showOptions = false

    var body: some View {
        VStack {
            Button("Rename") { showRename = true }
            Button("More Options") { showOptions = true }
        }
        .alert("Rename Item", isPresented: $showRename) {
            TextField("New name", text: $newName)
            Button("Save") { /* persist newName */ }
            Button("Cancel", role: .cancel) { }
        }
        .confirmationDialog("Item Options", isPresented: $showOptions, titleVisibility: .visible) {
            Button("Duplicate") { }
            Button("Delete", role: .destructive) { }
            Button("Cancel", role: .cancel) { }
        }
    }
}

Migrating off the deprecated Alert() type

Older SwiftUI code returned Alert values from an .alert(isPresented:content:) closure using Alert(title:message:primaryButton:secondaryButton:). That whole family is deprecated. The migration is mechanical: replace the returned Alert with an actions closure of Buttons and a message closure of Text. Alert.Button.destructive becomes Button(role: .destructive), and .cancel maps to Button(role: .cancel). The payoff is more than three buttons (the old struct capped at two), TextField support, and consistency with .confirmationDialog. If you still see Alert() in a codebase, treat it as tech debt worth clearing during any touch of that file.

  • Alert(title:message:dismissButton:) → .alert(title, isPresented:) { Button("OK") { } }
  • primaryButton/secondaryButton → two Button views in the actions closure
  • .actionSheet → .confirmationDialog
  • The new API removes the two-button limit and unlocks text fields

Ship your SwiftUI app in 5 emails

A free 5-part course on the parts that actually stall launches: paywall, auth, onboarding, App Store review, and pricing. No fluff, unsubscribe anytime.

Stop hand-rolling error dialogs

The Swift Kit ships a typed AppError enum bound to a single item-driven alert, so every thrown error surfaces a friendly, localized message with zero boilerplate per screen.

Get The Swift Kit — $99

Rather have it done for you? I set up and ship your app from $499. See how

How to present an error alert that carries data

Use an optional @State so the alert both presents and self-clears, and so the message reflects the specific failure.

  1. 1

    Model the error

    Create a small Identifiable struct or enum that holds a user-facing title and message.

    struct AppError: Identifiable { let id = UUID(); let title: String; let message: String }
  2. 2

    Hold it in optional state

    Declare an @State optional. nil means no alert; a value means present.

    @State private var activeError: AppError?
  3. 3

    Attach an item alert

    Bind the optional to .alert(item:) and format the buttons and message from the unwrapped value.

    .alert(item: $activeError) { err in
        Alert(title: Text(err.title), message: Text(err.message))
    }
  4. 4

    Trigger from a catch

    In your async work, assign activeError inside the catch block on the main actor.

    do { try await upload() }
    catch { activeError = AppError(title: "Failed", message: error.localizedDescription) }

Frequently Asked Questions

Why does my SwiftUI .alert(isPresented:) show blank or not appear when I attach it inside a ForEach row?
Attaching a Bool-driven alert to each row of a ForEach means multiple alerts fight over presentation and SwiftUI often shows none or the wrong one. Move a single .alert to the parent container and drive it with an optional item (.alert(item:)) that identifies which row triggered it, rather than one boolean per row.
How do I add more than two buttons to a SwiftUI alert now that Alert() is deprecated?
The old Alert() struct capped you at a primary and secondary button. The modern .alert(_:isPresented:) actions closure has no such limit — just declare as many Button views as you need inside it. For genuinely long option lists prefer .confirmationDialog or a Menu instead of stacking many alert buttons.
Can I put a TextField inside a SwiftUI alert, and from which iOS version?
Yes. Since iOS 16 you can place a TextField or SecureField directly in the .alert actions closure and bind it to @State. Read the bound value in your confirm button's action. Only short single-line input is appropriate; anything longer belongs in a sheet.
Why does my SwiftUI destructive alert button appear in a different position than I wrote it in code?
SwiftUI positions .cancel and .destructive role buttons according to the platform's Human Interface Guidelines, not your declaration order. Assign roles correctly and let the system place them — never hard-code layout assumptions based on the order you wrote the buttons.
What replaces .actionSheet in SwiftUI for showing a bottom action sheet?
Use .confirmationDialog(_:isPresented:titleVisibility:actions:). It supersedes the deprecated .actionSheet, supports button roles, renders as a sheet on iPhone and a popover on iPad, and lets you hide or show the title via titleVisibility.

Keep exploring

Build on a foundation that handles the edge cases

The Swift Kit is a $99 one-time SwiftUI boilerplate with Supabase auth, RevenueCat paywalls, multi-provider AI, and a design system — including a typed error-alert layer so you ship polished dialogs from day one.

Get The Swift Kit — $99

One-time purchase · Lifetime updates · 14-day refund