- Published on
> Presenting Multiple Sheets in SwiftUI
- Authors

- Name
- Mick MacCallum
- @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.
// Continue_Learning
How to force navigation bar to show background in SwiftUI?
Learn how to force the navigation bar to show a background color in SwiftUI.
Supporting Dark Mode in a SwiftUI App
Learn how to properly support dark mode in SwiftUI using semantic colors, adaptive color assets, and color scheme detection.
Using SF Symbols in SwiftUI
Learn how to use SF Symbols in your SwiftUI apps, including sizing, coloring, animations, and finding the right symbol for your needs.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.