SwiftUI's new item based presentations in iOS 27

June 20, 2026

At WWDC 2026, Apple introduced several small SwiftUI APIs that are easy to overlook.

None of them add a new visual component.

None of them introduce new capabilities that were previously impossible.

Yet together they represent one of the most meaningful refinements to SwiftUI's presentation model since sheet(item:) debuted.

SwiftUI has always encouraged developers to derive presentation from data.

If data exists, something appears.

If the data disappears, the presentation goes away.

For years, this philosophy was already visible in APIs such as:

.sheet(item:)
.popover(item:)
.navigationDestination(item:)

However, two commonly used presentation APIs remained inconsistent.

  • alert
  • confirmationDialog

Until iOS 27, presenting either of these usually required maintaining two independent pieces of state.

@State private var selectedInvoice: Invoice?
@State private var showingAlert = false

Or perhaps:

@State private var invoiceToDelete: Invoice?
@State private var showingDeleteDialog = false

At WWDC 2026, SwiftUI finally completes the picture.

iOS 27 introduces item-based overloads for alerts and confirmation dialogs.

confirmationDialog(_:item:titleVisibility:actions:)

confirmationDialog(_:item:titleVisibility:actions:message:)

alert(_:item:actions:message:)

alert(_:item:actions:)

alert(error:actions:)

alert(error:actions:message:)

Together these APIs make it possible to model presentations entirely from optional state.

No extra Boolean.

No synchronization bugs.

No impossible states.


Why isPresented was always slightly awkward

Consider an invoicing application.

Users can delete invoices from a list.

Before iOS 27, the implementation often looked like this.

@State private var invoiceToDelete: Invoice?
@State private var showingDeleteConfirmation = false

When the user taps Delete:

invoiceToDelete = invoice
showingDeleteConfirmation = true

And the confirmation dialog:

.confirmationDialog(
    "Delete Invoice",
    isPresented: $showingDeleteConfirmation
) {
    if let invoice = invoiceToDelete {
        Button("Delete", role: .destructive) {
            delete(invoice)
        }
    }
}

There is an obvious issue here.

Both properties represent the same thing.

If an invoice exists, the dialog should appear.

If there is no invoice, the dialog should not exist.

Unfortunately, invalid states become possible.

invoiceToDelete = nil
showingDeleteConfirmation = true

Or:

invoiceToDelete = invoice
showingDeleteConfirmation = false

SwiftUI encourages developers to model state in ways that prevent impossible situations.

The old approach allows impossible situations.

The new APIs solve this problem completely.


Confirmation dialogs using item bindings

SwiftUI now provides two overloads for item-based confirmation dialogs.

func confirmationDialog<A, T>(
    _ title: Text,
    item data: Binding<T?>,
    titleVisibility: Visibility = .automatic,
    @ContentBuilder actions: (T) -> A
) -> some View where A : View

and

func confirmationDialog<A, M, T>(
    _ title: Text,
    item data: Binding<T?>,
    titleVisibility: Visibility = .automatic,
    @ContentBuilder actions: (T) -> A,
    @ContentBuilder message: (T) -> M
) -> some View where A : View, M : View

Whenever the bound item becomes non-nil, SwiftUI presents the confirmation dialog.

Once dismissed, SwiftUI automatically resets the binding back to nil.

Example: Deleting an Invoice

struct Invoice: Identifiable {
    let id: UUID
    let number: String
}

State becomes remarkably simple.

@State private var invoiceToDelete: Invoice?

Triggering presentation:

Button("Delete") {
    invoiceToDelete = invoice
}

Dialog presentation:

.confirmationDialog(
    "Delete Invoice",
    item: $invoiceToDelete,
    titleVisibility: .visible
) { invoice in
    Button("Delete \"\(invoice.number)\"", role: .destructive) {
        delete(invoice)
    }
    Button("Cancel", role: .cancel) { }
} message: { invoice in
    Text("Invoice \(invoice.number) will be permanently removed.")
}

Several improvements immediately stand out.

There is no Boolean state.

No optional unwrapping.

No manual cleanup.

No need to keep two states synchronized.

SwiftUI manages everything automatically.


Item based Alerts

iOS 27 also introduces two item-based alert overloads.

alert(_:item:actions:message:)

This overload behaves similarly to sheet(item:).

SwiftUI presents an alert whenever the item becomes non-nil.

func alert<A, M, T>(
    _ titleKey: LocalizedStringKey,
    item data: Binding<T?>,
    @ContentBuilder actions: (T) -> A,
    @ContentBuilder message: (T) -> M
) -> some View where A : View, M : View

Example: Payment Failure Alert

struct PaymentFailure: Identifiable {
    let id = UUID()
    let invoiceNumber: String
    let reason: String
}

State:

@State private var paymentFailure: PaymentFailure?

Presentation:

paymentFailure = PaymentFailure(
    invoiceNumber: invoice.number,
    reason: "Network connection lost"
)

Alert:

.alert(
    "Payment Failed",
    item: $paymentFailure
) { error in
    Button("Retry") {
        retryPayment()
    }

    Button("Cancel", role: .cancel) { }
} message: { error in
    Text(
        """
        Unable to process invoice \
        \(error.invoiceNumber).

        \(error.reason)
        """
    )
}

alert(_:item:actions:)

Sometimes an alert doesn't require additional explanatory text.

func alert<A, T>(
    _ title: Text,
    item data: Binding<T?>,
    @ContentBuilder actions: (T) -> A
) -> some View where A : View

This overload removes the message closure.

Example: Duplicate Invoice Detection

struct DuplicateInvoice: Identifiable {
    let id = UUID()
    let number: String
}

State:

@State private var duplicateInvoice: DuplicateInvoice?

Trigger alert:

duplicateInvoice = DuplicateInvoice(
    number: "INV-2026-001"
)

Presentation:

.alert(
    "Invoice Already Exists",
    item: $duplicateInvoice
) { invoice in
    Button("Replace") {
        replaceInvoice(invoice)
    }

    Button("Keep Existing", role: .cancel) { }
}

This overload works well when the title itself already provides enough context.


Error based Alerts

SwiftUI also includes dedicated APIs for presenting errors directly.

Instead of wrapping errors inside custom Identifiable models, SwiftUI can now derive alert presentation from an optional error value.

alert(error:actions:)

func alert<E, A>(
    error: Binding<E?>,
    @ContentBuilder actions: () -> A
) -> some View where E : Error, A : View

Example: Upload Failure

enum UploadError: Error {
    case fileTooLarge
    case insufficientStorage
}

State:

@State private var uploadError: UploadError?

Presentation:

uploadError = .insufficientStorage

Alert:

.alert(
    error: $uploadError
) { error in
    Button("OK") { }
}

alert(error:actions:message:)

func alert<E, A, M>(
    error: Binding<E?>,
    @ContentBuilder actions: (E) -> A,
    @ContentBuilder message: (E) -> M
) -> some View where E : Error, A : View, M : View

This overload provides access to the error value inside a message closure.

Example: Cloud Sync Failure

enum SyncError: Error {
    case networkUnavailable
    case unauthorized
}

State:

@State private var syncError: SyncError?

Presentation:

syncError = .networkUnavailable

Alert:

.alert(
    error: $syncError
) { error in
    Button("Retry") {
        sync()
    }

    Button("Cancel", role: .cancel) { }
} message: { error in
    switch error {
    case .networkUnavailable:
        Text("Check your internet connection and try again.")
    case .unauthorized:
        Text("Please sign in again.")
    }
}

Which API should you use?

API Best Used For
confirmationDialog(item:actions:) Destructive operations with contextual information
confirmationDialog(item:actions:message:) Destructive operations requiring additional explanation
alert(item:actions:) Contextual alerts where the title is sufficient
alert(item:actions:message:) Contextual alerts requiring detailed messages
alert(error:actions:) Simple error presentation
alert(error:actions:message:) Error handling with customized descriptions

Final Thoughts

I think this is one of the most underrated SwiftUI improvements announced at WWDC 2026.

These APIs don't introduce new capabilities.

Developers could already build alerts and confirmation dialogs before.

Instead, they remove a surprisingly common state-management smell found in many SwiftUI applications.

I've reviewed numerous codebases where presentation state looked like this:

@State private var selectedItem: Item?
@State private var showingAlert = false

or

@Presents var destination: Destination.State?

while the destination itself already contained enough information to determine whether presentation should happen.

With iOS 27, SwiftUI encourages a much simpler mental model:

Presentation is just another consequence of data.

If an item exists, present it.

If an error exists, show it.

If the item disappears, the presentation disappears as well.

That philosophy has been gradually spreading throughout SwiftUI since its introduction, and iOS 27 finally feels like the release where Apple completed the family.

If you have suggestions or opinion, feel free to connect with me on X and send me a DM. If this article helped you, Buy me a coffee.