BS
BleepingSwift
Published on

> Presenting Multiple Sheets in SwiftUI

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

SwiftUI makes presenting a single sheet easy, but what happens when you need to show a second sheet from within the first? The typical approach—attaching multiple .sheet() modifiers to the same parent view—doesn't work reliably. SwiftUI expects only one sheet presentation per view. The solution is nesting your sheet modifiers so each sheet is responsible for presenting the next.

The Problem with Multiple Sheet Modifiers

You might try something like this:

struct ContentView: View {
    @State private var showFirstSheet = false
    @State private var showSecondSheet = false

    var body: some View {
        Button("Show First Sheet") {
            showFirstSheet = true
        }
        .sheet(isPresented: $showFirstSheet) {
            Text("First Sheet")
        }
        .sheet(isPresented: $showSecondSheet) {
            Text("Second Sheet")
        }
    }
}

This won't behave as expected. SwiftUI only supports a single sheet presentation per view at a time. If you trigger both, only one appears, and you may see warnings in the console.

The Nested Sheet Pattern

The correct approach is to attach the second sheet modifier inside the first sheet's content:

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

    var body: some View {
        Button("Show First Sheet") {
            showFirstSheet = true
        }
        .sheet(isPresented: $showFirstSheet) {
            FirstSheetView()
        }
    }
}

struct FirstSheetView: View {
    @State private var showSecondSheet = false

    var body: some View {
        VStack(spacing: 20) {
            Text("This is the first sheet")

            Button("Show Second Sheet") {
                showSecondSheet = true
            }
        }
        .sheet(isPresented: $showSecondSheet) {
            Text("This is the second sheet")
        }
    }
}

Now when you tap the button in the first sheet, the second sheet presents on top of it. Each view is responsible for presenting its own sheet, keeping the hierarchy clean.

Using an Enum for Sheet Types

When a single view might present different sheets depending on context, use an enum with sheet(item:):

struct ContentView: View {
    @State private var activeSheet: SheetType?

    enum SheetType: Identifiable {
        case settings
        case profile
        case help

        var id: Self { self }
    }

    var body: some View {
        VStack(spacing: 20) {
            Button("Settings") { activeSheet = .settings }
            Button("Profile") { activeSheet = .profile }
            Button("Help") { activeSheet = .help }
        }
        .sheet(item: $activeSheet) { type in
            switch type {
            case .settings:
                SettingsView()
            case .profile:
                ProfileView()
            case .help:
                HelpView()
            }
        }
    }
}

This pattern handles any number of different sheet destinations with a single .sheet() modifier. The sheet dismisses automatically when activeSheet becomes nil.

Nested Sheets with Enum Pattern

Combine both patterns when sheets themselves need to present additional sheets:

struct SettingsView: View {
    @State private var detailSheet: DetailType?

    enum DetailType: Identifiable {
        case account
        case notifications
        case privacy

        var id: Self { self }
    }

    var body: some View {
        NavigationStack {
            List {
                Button("Account Settings") { detailSheet = .account }
                Button("Notifications") { detailSheet = .notifications }
                Button("Privacy") { detailSheet = .privacy }
            }
            .navigationTitle("Settings")
        }
        .sheet(item: $detailSheet) { type in
            switch type {
            case .account:
                AccountDetailView()
            case .notifications:
                NotificationSettingsView()
            case .privacy:
                PrivacySettingsView()
            }
        }
    }
}

Each level of sheet manages its own presentation state, creating a clean hierarchy that SwiftUI can manage correctly.

Passing Dismiss Actions Down

When a nested sheet needs to dismiss multiple levels, pass dismiss actions through the environment or as closures:

struct FirstSheetView: View {
    @Environment(\.dismiss) var dismissFirst
    @State private var showSecondSheet = false

    var body: some View {
        VStack {
            Text("First Sheet")
            Button("Open Second") { showSecondSheet = true }
        }
        .sheet(isPresented: $showSecondSheet) {
            SecondSheetView(dismissAll: {
                showSecondSheet = false
                dismissFirst()
            })
        }
    }
}

struct SecondSheetView: View {
    let dismissAll: () -> Void

    var body: some View {
        VStack {
            Text("Second Sheet")
            Button("Done") {
                dismissAll()
            }
        }
    }
}

The dismissAll closure first sets showSecondSheet to false (dismissing the second sheet), then calls dismissFirst() to dismiss the first sheet as well.

Full-Screen Covers

The same nesting pattern applies to .fullScreenCover():

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

    var body: some View {
        Button("Present") { showModal = true }
            .fullScreenCover(isPresented: $showModal) {
                ModalView()
            }
    }
}

struct ModalView: View {
    @State private var showNestedSheet = false
    @Environment(\.dismiss) var dismiss

    var body: some View {
        VStack {
            Button("Show Sheet") { showNestedSheet = true }
            Button("Close") { dismiss() }
        }
        .sheet(isPresented: $showNestedSheet) {
            Text("A sheet inside a full-screen cover")
        }
    }
}

You can nest sheets inside full-screen covers, or full-screen covers inside sheets—the key is that each presentation modifier lives on a different view in the hierarchy.

The constraint of one sheet per view might feel limiting at first, but the nested pattern it encourages actually leads to better-organized code where each view manages only its own immediate presentations.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.