Dismissing SwiftUI Views

SwiftUI has a less clumsy mechanism for dismissing presented views in iOS 15.

Presentation Mode (iOS 13/14)

Before iOS 15, if you wanted to programmatically dismiss a presented view you used the presentationMode environment property. For example, here’s a view where I present a modal sheet from the button action (some details omitted):

struct ItemView: View {
  @EnvironmentObject var store: ItemStore
  @State private var isShowingCart = false
    
  var body: some View {
    VStack {
      ...
      
      Button(action: addToCart) {
        Text("Add To Cart")
      }
    }
    .sheet(isPresented: $isShowingCart) {
      CartView()
    }
  }
}

Note: To show the view full screen, replace the .sheet modifier with .fullScreenCover:

.fullScreenCover(isPresented: $isShowingCart) {

SwiftUI shows the sheet when we set the state variable isShowingCart to true in the addToCart method:

private func addToCart() {
  store.addToCart(item)
  isShowingCart = true
}

This presents a modal sheet which you can dismiss by dragging down on the sheet. If you want to dismiss the sheet programmatically you get the presentation mode from the environment:

@Environment(\.presentationMode) var presentationMode

Then you call the dismiss() method on the wrapped value:

presentationMode.wrappedValue.dismiss()

For example, if I have cancel and save buttons on my presented cart view:

Modal Cart view with cancel and save toolbar buttons

struct CartView: View {
  @Environment(\.presentationMode) var presentationMode

  var body: some View {
    NavigationView {
      List { ... }
      .toolbar {
        ToolbarItem(placement: .primaryAction) {
            Button("Save") {
                store.save()
                dismiss()
            }
        }          
        ToolbarItem(placement: .cancellationAction) {
            Button("Cancel", role: .cancel) {
                dismiss()
            }
        }
      }
    }
  }

  private func dismiss() {
    presentationMode.wrappedValue.dismiss()
  }
}

That always seemed a clumsy mechanism to me so I’m happy we have something better in iOS 15.

Dismiss Action (iOS 15)

In iOS 15, Apple deprecated the presentationMode environment property, replacing it with a dismiss action. You get the dismiss action from the environment of the presented view:

@Environment(\.dismiss) private var dismiss

You then call the dismiss action directly with no need to mess with wrapped values:

dismiss()

For example, my cart view updated for iOS 15:

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

  var body: some View {
    NavigationView {
      List { ... }
      .toolbar {
        ToolbarItem(placement: .primaryAction) {
          Button("Checkout") {
            store.save()
            dismiss()
          }
        }        
        ToolbarItem(placement: .cancellationAction) {
          Button("Cancel", role: .cancel) {
            dismiss()
          }
        }
      }
    }
  }
}

Note: Calling dismiss() on a view that’s not presented has no effect.

If you want to check if you’re presenting a view use the isPresented environment property in the view:

@Environment(\.isPresented) private var isPresented

This replaces the property of the same name from the now deprecated presentationMode. For example, to do an action when a view is first presented:

.onChange(of: isPresented) { isPresented in
  if isPresented {
    store.prepareCart()
  }
}