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)
}
}
# 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()
}
}
}
If you have older iOS apps and want to enhance them with modern SwiftUI features, check out my book Integrating SwiftUI into UIKit Apps. It provides detailed guidance on gradually adopting SwiftUI in your UIKit projects. Additionally, if you're eager to enhance your Swift programming skills, my latest book Swift Gems offers over a hundred advanced tips and techniques, including optimizing collections, handling strings, mastering asynchronous programming, and debugging, to take your Swift code to the next level.