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)
    }
}
Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35

# 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.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Enhance older apps with SwiftUI!$45

A detailed guide on gradually adopting SwiftUI in UIKit projects

Updated for iOS 18 and Xcode 16!

Integrating SwiftUI into UIKit Appsby Natalia Panferova

  • Upgrade your apps with new features like Swift Charts and Widgets
  • Support older iOS versions with effective backward-compatible strategies
  • Seamlessly bridge state and data between UIKit and SwiftUI using the latest APIs

Enhance older apps with SwiftUI!

A detailed guide on gradually adopting SwiftUI in UIKit projects

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Integrating SwiftUI
into UIKit Apps

by Natalia Panferova

Updated for iOS 18 and Xcode 16!

$45

# 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.