Using the dismiss action from the SwiftUI environment

Starting from iOS 15 and macOS 12 SwiftUI provides the dismiss environment value that can be used to programmatically dismiss presentations. This was introduced as a replacement for the older presentationMode binding that was a bit confusing and tricky to use.

The dismiss environment value stores a DismissAction. It's a struct but we can call it directly because it defines a callAsFunction() method. This method is called implicitly when we simply write dismiss().

struct SheetContent: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        Button("Dismiss") {
            dismiss()
        }
    }
}

If you prefer to pass the action to the button directly instead of calling it, you would need to provide it the callAsFunction method. We can't pass the DismissAction stored in the dismiss environment value to the button, because it's a struct and not a function.

struct SheetContent: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        Button("Dismiss", action: dismiss.callAsFunction)
    }
}
Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Check out our book!

Integrating SwiftUI into UIKit Apps

Integrating SwiftUI intoUIKit Apps

UPDATED FOR iOS 17!

A detailed guide on gradually adopting SwiftUI in UIKit projects.

  • Discover various ways to add SwiftUI views to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit

# Dismissing sheets and popovers

We can use the dismiss environment value to programmatically dismiss sheets and popovers in SwiftUI. It's important to read the value inside of the sheet/popover content. To get the right action, we have to wrap the contents of the presentation in a separate view.

struct ContentView: View {
    @State private var showSheet = false
    
    var body: some View {
        Button("Show sheet") {
            showSheet = true
        }
        .sheet(isPresented: $showSheet) {
            SheetContent()
        }
    }
}

struct SheetContent: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        Button("Dismiss") {
            dismiss()
        }
    }
}

If we were to read the dismiss environment value inside of the ContentView instead, calling it wouldn't have any effect on the sheet. In that case the dismiss value would refer to the ContentView, and since the ContentView is not presented, the call of the function would be ignored by SwiftUI.

Dismissing a popover works the exact same way. If we read the dismiss environment value inside of the popover and call it programmatically, the popover will be dismissed.

struct ContentView: View {
    @State private var showPopover = false

    var body: some View {
        Button("Show popover") {
            showPopover = true
        }
        .popover(isPresented: $showPopover) {
            PopoverContent()
        }
    }
}

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

    var body: some View {
        Button("Dismiss") {
            dismiss()
        }
    }
}

# Popping views from the navigation stack

We can use the dismiss action in the environment to programmatically pop views from the navigation stack as well. In this case we have to make sure that we read the environment inside of the detail view that we want to pop.

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Show detail") {
                DetailView()
            }
        }
    }
}

struct DetailView: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        Button("Pop") {
            dismiss()
        }
    }
}

We can use this method to pop detail views one at a time. If you are pushing multiple views onto a navigation stack, and need to return to the root view programmatically in one single action, you would need to use programmatic navigation as described in one of my previous posts on navigation in SwiftUI.

Note that the NavigationStack API is only available starting from iOS 16 and macOS 13. On iOS 15 the dismiss action works in a similar way with the NavigationView when it's using the StackNavigationViewStyle.

# Dismissing UIHostingController from SwiftUI

We can even use the dismiss action provided in the SwiftUI environment to dismiss a UIHostingController presented from UIKit. When we integrate SwiftUI into a UIKit app using the UIHostingController API, the SwiftUI view is aware of its presentation status and can dismiss itself even when it's presented inside a UIKit view controller using UIKit presentation APIs.

class ViewController: UIViewController {
    ...
    
    @IBAction func showSheet(_ sender: Any) {
        let swiftUIView = MyView()
        let controller = UIHostingController(rootView: swiftUIView)
        present(controller, animated: true)
    }
}

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

    var body: some View {
        Button("Dismiss") {
            dismiss()
        }
    }
}

We can also use the same technique if the UIHostingController that contains the SwiftUI view is pushed onto a navigation stack instead. Calling the dismiss action in the SwiftUI environment will pop the detail view.

class ViewController: UIViewController {
    ...
    
    @IBAction func showDetail(_ sender: Any) {
        let swiftUIView = MyView()
        let controller = UIHostingController(rootView: swiftUIView)
        navigationController?.pushViewController(
            controller, animated: true
        )
    }
}

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

    var body: some View {
        Button("Pop") {
            dismiss()
        }
    }
}

If you are working on integrating SwiftUI into an existing UIKit project, check out my recently released book Integrating SwiftUI into UIKit Apps. It shows a variety of ways to add SwiftUI to an existing app built with UIKit, so that you can take advantage of the new APIs like, for example Swift Charts, and add new features faster. It also covers how to set up data flow between the UIKIt and the SwiftUI parts of the project, how to size and position embedded SwiftUI views and how to make the most of the previews in Xcode while developing your app.

# Dismissing presentations on iOS 14

If you need to support iOS versions that are older than iOS 15, you would need to fall back to the old PresentationMode API. The presentationMode environment value stores a binding to the PresentationMode which in turn has a dismiss() method to dismiss the presentation. To be able to call that action, we need to read the presentation mode from the environment using the @Binding property wrapper.

struct SheetContent: View {
    @Environment(\.presentationMode) @Binding
    private var presentationMode

    var body: some View {
            Button("Dismiss") {
                presentationMode.dismiss()
            }
    }
}

Another way to get to the action would be to go via the wrappedValue of the PresentationMode binding.

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

    var body: some View {
            Button("Dismiss") {
                presentationMode
                    .wrappedValue.dismiss()
            }
    }
}