I have a SwiftUI view that will be presented inside a UIHostingController
in a larger app. I want the SwiftUI view to be able to dismiss itself when the user taps a "Done" button in its toolbar. As I understand it, this can be done with the DismissAction environment variable.
Unfortunately, dismiss
seems to be a no-op for me. I created an isolated example in a fresh project to re-create the problem. Here it is:
struct TestView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
Text("Hello, World!")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let navController = UINavigationController(rootViewController: UIHostingController(rootView: TestView()))
self.present(navController, animated: true)
}
}
I'm using an example XCode storyboard project, which creates ViewController
as the window's root controller.
When I run the app, the viewDidAppear
method is called on ViewController
, and it shows a navigation controller containing my SwiftUI view.
However, nothing at all happens when I tap the "Done" button. What am I missing here?
The dismiss
action of a hosting VC embedded in a UINavigationController
would try to pop the currently presented VC. Since your hosting VC is already the root, nothing happens. The hosting VC won't look up its hierarchy and see that it is being presented modally.
Instead of using a UINavigationController
, embed the SwiftUI view in a SwiftUI NavigationStack
, and present that directly.
struct Root: View {
var body: some View {
NavigationStack {
TestView()
}
}
}
self.present(UIHostingController(rootView: Root()), animated: true)
If you must use a UINavigationController
, you can inject your own dismiss
environment value:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let navController = UINavigationController()
navController.viewControllers = [UIHostingController(
rootView: TestView()
.environment(\.customDismiss) {
navController.dismiss(animated: true)
}
)]
self.present(navController, animated: true)
}
extension EnvironmentValues {
@Entry var customDismiss: @MainActor () -> Void = {}
}
// In the SwiftUI view you need to read customDimiss,
@Environment(\.customDismiss) var dismiss
If you also want to reuse TestView
in a SwiftUI environment, it might be desirable to have customDismiss
do whatever dismiss
would have done if customDismiss
is not set. In that case, write a custom EnvironmentValues
property like this, instead of using @Entry
,
struct CustomDismissKey: EnvironmentKey {
static let defaultValue: (@MainActor () -> Void)? = nil
}
extension EnvironmentValues {
var customDismiss: @MainActor () -> Void {
get {
self[CustomDismissKey.self] ?? self.dismiss.callAsFunction
}
set {
self[CustomDismissKey.self] = newValue
}
}
}