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.
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.
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.
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.")
}
}
}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.
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.
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.
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
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
Hold it in optional state
Declare an @State optional. nil means no alert; a value means present.
@State private var activeError: AppError? - 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
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?
How do I add more than two buttons to a SwiftUI alert now that Alert() is deprecated?
Can I put a TextField inside a SwiftUI alert, and from which iOS version?
Why does my SwiftUI destructive alert button appear in a different position than I wrote it in code?
What replaces .actionSheet in SwiftUI for showing a bottom action sheet?
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 — $99One-time purchase · Lifetime updates · 14-day refund