- Published on
- 4 min read
> Presenting Multiple Sheets in SwiftUI
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.
SwiftUI Animations: From Basics to KeyframeAnimator
A practical guide to SwiftUI animations covering implicit and explicit animations, spring physics, PhaseAnimator for multi-step sequences, and KeyframeAnimator for timeline-based control.
How to Change the Background Color of a View in SwiftUI
Learn different ways to set background colors on SwiftUI views, from simple color fills to gradients and materials.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.