SwiftUI Sheet Not Dismissing: Causes & Fix
A SwiftUI sheet doesn't dismiss when dismiss() is called, often due to incorrect environment usage or state management.
The Problem
When using a sheet in SwiftUI, it may fail to dismiss even when calling dismiss(). Here's an example of the broken implementation:
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented = true
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
}
}
}
struct SheetView: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss") {
presentationMode.wrappedValue.dismiss()
}
}
}
Root Cause
The issue often stems from incorrect use of the presentationMode environment variable or SwiftUI's sheet binding mechanics. In particular, SwiftUI does not always propagate the correct presentation context when the sheet is presented via a binding. This can lead to the presentationMode.dismiss() call having no effect because it doesn't reference the actual presented view controller.
Additionally, if the isPresented binding is not updated correctly or if it's being observed in a way that SwiftUI doesn't track changes (e.g., from an external object without proper @State or @Binding propagation), the system may not recognize that the sheet should be dismissed.
The Fix
Ensure the sheet's binding is properly managed and that the presentation mode is used in the correct context. Here's the corrected version:
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
Button("Show Sheet") {
isSheetPresented = true
}
.sheet(isPresented: $isSheetPresented) {
SheetView(isPresented: $isSheetPresented)
}
}
}
struct SheetView: View {
@Binding var isPresented: Bool
var body: some View {
Button("Dismiss") {
isPresented = false
}
}
}
// ✅ Fixed: Instead of relying solely on presentationMode, bind directly to the state controlling the sheet.
Alternative Approaches
-
Using
presentationModewith a custom environment binding: If you prefer to usepresentationMode, ensure the sheet's content view is part of the same environment where the presentation context is active. -
Using
@ObservedObjector@StateObjectfor shared state: You can manage the sheet's presentation state via a shared view model that both the parent and sheet view observe.
class SheetViewModel: ObservableObject {
@Published var isPresented = false
}
// In ContentView:
@StateObject private var viewModel = SheetViewModel()
.sheet(isPresented: $viewModel.isPresented) {
SheetView(viewModel: viewModel)
}
Affected iOS Versions
This behavior affects iOS 13 through iOS 15. While not officially "fixed", SwiftUI's behavior in iOS 16 improved with the introduction of NavigationStack and better presentation APIs, which can reduce reliance on presentationMode for dismissal.