VButko

How to dismiss SwiftUI modal View embedded in a UIHostingController

Download materials

Adding SwiftUI views into a UIViewController is very easy using UIHostingController and encourages us to start using SwiftUI within UIKit apps. Eventually, you'll need to dismiss the view, which can be done in a number of ways.

In this article, let's look at some most obvious ways of dismissing modal SwiftUI views:

  • Using SwiftUI presentationMode environment value
  • Using SwiftUI dismiss environment value for iOS 15 and above
  • Providing a dismiss action from the presenting UIViewController

You can download a sample project on GitHub.

Presenting SwiftUI view

Before looking at the ways we can dismiss a SwiftUI view, let's embed it into a UIHostingController and present it from a UIViewController. We will also add a "gear" navigation bar button that triggers the presentation:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        configureViewController()
    }

    private func configureViewController() {
        title = "Dismiss SwiftUI Modal View"
        configureNavigationBar()
    }
    
    private func configureNavigationBar() {
        let presentSettingsAction = UIAction { _ in
            self.presentSwiftUISettingsScreen()
        }
        
        let settingsButton = UIBarButtonItem(
            image: UIImage(systemName: "gear"),
            primaryAction: presentSettingsAction
        )
        
        navigationItem.rightBarButtonItem = settingsButton
    }
    
    private func presentSwiftUISettingsScreen() {
        let settingsScreen = SettingsScreen()
        let hostingController = UIHostingController(rootView: settingsScreen)
        present(hostingController, animated: true, completion: nil)
    }
}

Once the view is presented, let's add a dismiss button to it.

Using SwiftUI presentationMode environment value

The best way to dismiss the SwiftUI view is to use presentationMode environment value. The benefit of using presentationMode is that it works for any presentation flow: pushing onto a navigation stack and displaying as a modal screen.

Dismissing the SwiftUI view using environment value works from any presentation context: when presenting from the SwiftUI view and from UIKit.

The implementation is very straightforward. In your SwiftUI view add environment value and call its dismiss method when needed (for example, in the navigation bar button):

struct SettingsScreen: View {
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        NavigationView {
            Text("Hello, World!")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Dismiss") {
                            presentationMode.wrappedValue.dismiss()
                        }
                    }
                }
        }
    }
}

This is a very nice implementation and a recommended approach. However, in iOS 15 we get another, direct API.

Using SwiftUI dismiss environment value

If you are targeting iOS 15 and above, then you can use even more direct dismiss action from SwiftUI View:

@available(iOS 15.0, *)
struct SettingsScreen: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        NavigationView {
            Text("Hello, World!")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Dismiss") {
                            dismiss()
                        }
                    }
                }
        }
    }
}

Providing a dismiss action from the presenting UIViewController

Another approach that is commonly used is to pass a dismiss action when you are presenting a SwiftUI View.

You simply add a dismiss property to your SwiftUI view and call it in a Button's action:

struct SettingsScreen: View {
    var dismissAction: (() -> Void)
    
    var body: some View {
        NavigationView {
            Text("Hello, World!")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Dismiss") {
                            dismissAction()
                        }
                    }
                }
        }
    }
}

When you are presenting the View from UIKit, provide a dismiss action:

private func presentSwiftUISettingsScreen() {
    let settingsScreen = SettingsScreen(
        dismissAction: {
            self.dismiss(animated: true, completion: nil)
        }
    )
    
    let hostingController = UIHostingController(rootView: settingsScreen)
    present(hostingController, animated: true, completion: nil)
}

This approach is very similar to the pattern that is used in UIKit and can be used for any kind of action.

Conclusion

In this article, you've seen how to dismiss a SwiftUI view using different approaches. I would recommend using environment values as it is a native SwiftUI implementation. However, I wanted to show the last approach as a reference that can be used to trigger other basic actions that you have already implemented in a UIViewController and want to reuse from the SwiftUI context.

You can download a sample project with the implementation via the link at the top of the page.

I hope you enjoyed this article. If you have any questions, suggestions, or feedback, please let me know on Twitter.

Thanks for reading!