Introduction

SwiftUI sheets are controlled by a Binding<Bool> or Identifiable item. When the user swipes down to dismiss, or when .dismiss() is called, the binding should update to false. However, in certain scenarios the binding does not update correctly, causing the sheet to reappear, the presenting view's state to remain inconsistent, or the sheet to be stuck in a half-dismissed state.

Symptoms

  • Sheet dismisses visually but binding remains true
  • Sheet reappears immediately after dismissal
  • Programmatic dismissal with .dismiss() does not work
  • Binding updated but sheet does not dismiss
  • Full screen cover behaves differently from sheet

Debug presentation state: ```swift struct ContentView: View { @State private var isPresented = false

var body: some View { Button("Show") { isPresented = true } .sheet(isPresented: $isPresented) { Text("Sheet content") .onAppear { print("Sheet appeared") } .onDisappear { print("Sheet disappeared, isPresented = \(isPresented)") } } } } ```

Common Causes

  • State variable declared in child view instead of parent
  • Multiple bindings to the same presentation state
  • @State variable reset during view update cycle
  • Sheet content modifies the binding incorrectly
  • NavigationStack inside sheet interfering with dismissal

Step-by-Step Fix

  1. 1.**Manage presentation state in the presenting view":
  2. 2.```swift
  3. 3.// CORRECT - State in presenting view
  4. 4.struct ParentView: View {
  5. 5.@State private var showSheet = false

var body: some View { Button("Open") { showSheet = true } .sheet(isPresented: $showSheet) { SheetContentView() // Does NOT manage its own presentation } } }

struct SheetContentView: View { @Environment(\.dismiss) private var dismiss

var body: some View { VStack { Text("Sheet content") Button("Done") { dismiss() // Uses environment, does not manipulate binding directly } } } }

// WRONG - State in child view trying to control parent's sheet struct SheetContentViewWrong: View { @State private var showSheet = false // Different state variable!

var body: some View { VStack { Text("Sheet content") Button("Done") { showSheet = false // This does NOT dismiss the sheet } } } } ```

  1. 1.**Use Identifiable item for sheet presentation":
  2. 2.```swift
  3. 3.struct Item: Identifiable {
  4. 4.let id = UUID()
  5. 5.let title: String
  6. 6.}

struct ListView: View { @State private var selectedItem: Item?

var body: some View { List(items) { item in Button(item.title) { selectedItem = item } } .sheet(item: $selectedItem) { item in // selectedItem is automatically nil when dismissed DetailView(item: item) } } } ```

  1. 1.**Fix sheet that reappears after dismissal":
  2. 2.```swift
  3. 3.// Problem: sheet reappears because condition remains true
  4. 4.struct ProblematicView: View {
  5. 5.@State private var showSheet = true // Always true!

var body: some View { // Sheet keeps reappearing because showSheet never changes EmptyView() .sheet(isPresented: $showSheet) { Text("Sheet") } } }

// Fix: ensure state can be toggled struct FixedView: View { @State private var showSheet = false

var body: some View { Button("Show Sheet") { showSheet = true } .sheet(isPresented: $showSheet) { DismissableView() } } }

struct DismissableView: View { @Environment(\.dismiss) var dismiss

var body: some View { NavigationStack { Text("Content") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } } ```

  1. 1.**Handle nested sheet dismissal correctly":
  2. 2.```swift
  3. 3.struct ParentSheetView: View {
  4. 4.@Environment(\.dismiss) var dismiss
  5. 5.@State private var showChildSheet = false

var body: some View { NavigationStack { VStack { Text("Parent sheet") Button("Open child sheet") { showChildSheet = true } } .toolbar { Button("Dismiss all") { dismiss() } } } .sheet(isPresented: $showChildSheet) { ChildSheetView() } } } ```

Prevention

  • Always use @Environment(\.dismiss) in sheet content to dismiss
  • Keep presentation state (@State) in the presenting view
  • Use Identifiable item-based presentation for data-driven sheets
  • Avoid modifying the binding directly from within sheet content
  • Test sheet dismissal with both swipe gesture and programmatic dismiss
  • Use sheet(item:) instead of sheet(isPresented:) when presenting data items