```html SwiftUI: How to Implement Action Sheet (iOS 17+, 2026)

How to implement an action sheet in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: confirmationDialog Updated: May 11, 2026
TL;DR

Attach .confirmationDialog(_:isPresented:titleVisibility:actions:) to any view, drive it with a @State Bool, and place Button values (including destructive ones) inside the trailing closure. The old ActionSheet type is deprecated — confirmationDialog is the modern, accessible replacement.

struct ContentView: View {
    @State private var showSheet = false

    var body: some View {
        Button("Show Options") {
            showSheet = true
        }
        .confirmationDialog("What would you like to do?",
                            isPresented: $showSheet,
                            titleVisibility: .visible) {
            Button("Save") { }
            Button("Share") { }
            Button("Delete", role: .destructive) { }
            Button("Cancel", role: .cancel) { }
        }
    }
}

Full implementation

The example below models a realistic photo management scenario: selecting a photo triggers a sheet offering Save, Share, and Delete options. A separate piece of @State captures which action the user chose so you can react to it in your view model or elsewhere. The message parameter adds a subtitle beneath the dialog title for extra context when needed.

import SwiftUI

struct Photo: Identifiable {
    let id = UUID()
    let title: String
}

enum PhotoAction {
    case save, share, delete
}

struct PhotoRowView: View {
    let photo: Photo
    @State private var showActions = false
    @State private var lastAction: PhotoAction?

    var body: some View {
        HStack {
            Image(systemName: "photo.fill")
                .foregroundStyle(.secondary)
                .frame(width: 44, height: 44)
                .background(Color(.systemGray6))
                .clipShape(RoundedRectangle(cornerRadius: 8))

            VStack(alignment: .leading, spacing: 2) {
                Text(photo.title)
                    .font(.body)
                if let action = lastAction {
                    Text("Last: \(String(describing: action))")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }

            Spacer()

            Button {
                showActions = true
            } label: {
                Image(systemName: "ellipsis.circle")
                    .imageScale(.large)
                    .accessibilityLabel("More options for \(photo.title)")
            }
            .buttonStyle(.plain)
        }
        .padding(.vertical, 4)
        .confirmationDialog(
            "Photo Options",
            isPresented: $showActions,
            titleVisibility: .visible
        ) {
            Button("Save to Library") {
                lastAction = .save
            }

            Button("Share…") {
                lastAction = .share
            }

            Button("Delete Photo", role: .destructive) {
                lastAction = .delete
            }

            Button("Cancel", role: .cancel) { }
        } message: {
            Text("Choose an action for \"\(photo.title)\"")
        }
    }
}

struct PhotoListView: View {
    private let photos = [
        Photo(title: "Sunset at Big Sur"),
        Photo(title: "Morning Coffee"),
        Photo(title: "Weekend Hike"),
    ]

    var body: some View {
        NavigationStack {
            List(photos) { photo in
                PhotoRowView(photo: photo)
            }
            .navigationTitle("Photos")
        }
    }
}

#Preview {
    PhotoListView()
}

How it works

  1. @State private var showActions = false — A private Boolean drives the entire presentation lifecycle. Setting it to true presents the sheet; SwiftUI resets it to false automatically when the user dismisses the dialog (including tapping Cancel or the system dismiss gesture).
  2. .confirmationDialog(_:isPresented:titleVisibility:) — The modifier is attached directly to the view that logically owns the action, which keeps state and UI co-located. titleVisibility: .visible ensures the title renders above the buttons; pass .hidden if you only want the buttons shown.
  3. Button rolesrole: .destructive colours the Delete button red automatically and VoiceOver announces it as destructive — no manual styling required. role: .cancel always anchors to the bottom of the sheet as a separate button, matching iOS HIG conventions.
  4. message closure — The optional trailing message closure renders a subtitle line beneath the title. Use it to provide context (e.g., the specific item name) so users feel confident before tapping a destructive action.
  5. lastAction state — Capturing the selected action into @State (or forwarding it to a view model via a closure or @Binding) decouples your business logic from the presentation layer cleanly.

Variants

Triggered by a context menu long-press

You can combine contextMenu with confirmationDialog so a long-press reveals quick actions while a destructive path still gets confirmation.

struct ItemView: View {
    let title: String
    @State private var confirmDelete = false

    var body: some View {
        Text(title)
            .contextMenu {
                Button("Edit") { }
                Button("Duplicate") { }
                Divider()
                Button("Delete…", role: .destructive) {
                    confirmDelete = true
                }
            }
            .confirmationDialog(
                "Delete \"\(title)\"?",
                isPresented: $confirmDelete,
                titleVisibility: .visible
            ) {
                Button("Delete", role: .destructive) {
                    // perform deletion
                }
                Button("Cancel", role: .cancel) { }
            } message: {
                Text("This action cannot be undone.")
            }
    }
}

Dynamically built button list

When the available actions depend on runtime data (e.g., user permissions or item state), build buttons inside the closure using a ForEach over an array of action descriptors. The confirmationDialog actions closure is a @ViewBuilder, so standard control-flow constructs like if, ForEach, and Group all work inside it. Just keep total button count to seven or fewer to avoid the sheet becoming unwieldy — consider a Menu instead for longer lists.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement an action sheet in SwiftUI for iOS 17+.
Use confirmationDialog with titleVisibility: .visible.
Include Save, Share, and a destructive Delete option.
Make it accessible (VoiceOver labels on the trigger button).
Add a #Preview with realistic sample data.

Paste this prompt into Soarias during the Build phase after you've locked your screen flow — the generated component slots directly into your navigation stack without extra scaffolding.

Related

FAQ

Does this work on iOS 16?

Yes — confirmationDialog was introduced in iOS 15, so it works on iOS 15, 16, and 17+. This guide targets iOS 17+ because it uses Swift 5.10 syntax and the #Preview macro (Xcode 15+), but the confirmationDialog modifier itself requires no iOS 17-specific changes. If you need iOS 14 support, you can conditionally fall back to ActionSheet, but that API is deprecated and should be avoided in new code.

Can I have more than one confirmationDialog on the same view?

Yes, but each needs its own @State Bool binding. Only one can be presented at a time — triggering a second while the first is visible has no visible effect until the first is dismissed. A common pattern is to use a single @State enum (e.g., enum ActiveSheet) to represent which dialog to show, then switch over it in a single confirmationDialog modifier to keep your view tree clean.

What's the UIKit equivalent?

confirmationDialog maps directly to UIAlertController with preferredStyle: .actionSheet in UIKit. The SwiftUI modifier handles all the iPad/iPhone distinction for you (popover vs. sheet), whereas in UIKit you have to manually set popoverPresentationController.sourceView on iPad — a common source of crashes. confirmationDialog is strictly simpler.

Last reviewed: 2026-05-11 by the Soarias team.

```