How to Dismiss Sheet Programmatically in SwiftUI
Learn how to programmatically dismiss sheets in SwiftUI using presentationMode for iOS 15 and dismiss action for iOS 16+.
Overview
In SwiftUI, sheets are presented modally using the .sheet modifier, and by default, users dismiss them by swiping down or tapping outside the sheet. However, there are cases where you need to dismiss a sheet programmatically — for instance, after completing a form or handling a user action. This guide covers two primary methods: using presentationMode for iOS 15 and the newer dismiss API introduced in iOS 16.
The Approach
For iOS 15, we use presentationMode from the Environment to dismiss the sheet. Starting from iOS 16, Apple introduced a simpler dismiss method that doesn't require binding or managing the presentation state manually.
Here's a basic SwiftUI view that demonstrates both approaches:
import SwiftUI
struct ContentView: View {
@State private var isPresented = false
var body: some View {
Button("Present Sheet") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
if #available(iOS 16.0, *) {
DismissableView(isPresented: $isPresented)
} else {
DismissableViewLegacy(isPresented: $isPresented)
}
}
}
}
// iOS 16+
struct DismissableView: View {
@Binding var isPresented: Bool
var body: some View {
Button("Dismiss Sheet") {
isPresented = false
}
.padding()
}
}
// iOS 15 and earlier
struct DismissableViewLegacy: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss Sheet") {
presentationMode.wrappedValue.dismiss()
}
.padding()
}
}
Step-by-Step Breakdown
- ContentView: A main view that presents a sheet using
.sheet. It uses a@StatevariableisPresentedto control the presentation. - iOS Version Check: Inside the sheet, we check the iOS version and present the appropriate view (
DismissableViewfor iOS 16+ andDismissableViewLegacyfor iOS 15). - iOS 16+ Dismiss:
DismissableViewuses a@Bindingto theisPresentedstate and sets it tofalseto dismiss the sheet. - iOS 15 Dismiss:
DismissableViewLegacyuses@Environment(\.presentationMode)to access thepresentationModeenvironment value. Callingdismiss()on it programmatically dismisses the sheet.
This setup ensures backward compatibility while using the most modern approach available on supported devices.
Common Variations
1. Dismissing from a Nested View (iOS 16+)
Sometimes, you might need to dismiss a sheet from a deeply nested view. Here's how you can do it without passing the @Binding all the way down:
struct DismissableViewNested: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("Dismiss Sheet") {
dismiss()
}
}
}
This uses the dismiss environment value introduced in iOS 16, which is cleaner and avoids the need for a binding.
2. Dismissing with Animation (iOS 15+)
If you want to animate the dismissal (e.g., fade out), you can wrap the dismiss call in an animation block:
withAnimation {
isPresented = false
}
Or for iOS 15:
withAnimation {
presentationMode.wrappedValue.dismiss()
}
Pitfalls to Avoid
- Using
presentationModein iOS 16+: While it still works, it's deprecated in favor of thedismiss()environment value. Use the newer API for better clarity and future compatibility. - Overusing
@Binding: Passing a@Bindingthrough multiple layers can become cumbersome. Use@Environment(\.dismiss)in iOS 16+ for cleaner code. - Dismissing from Background Threads: Always ensure that dismissal logic runs on the main thread. If you're dismissing after a network request or async task, wrap it in
DispatchQueue.main.async.
Related Guides
- how-to-present-sheet-in-swiftui
- how-to-use-environment-in-swiftui
- how-to-handle-user-interactions-in-swiftui