'Dismiss a SwiftUI View that is contained in a UIHostingController
I have rewritten my sign in view controller as a SwiftUI View. The SignInView is wrapped in a UIHostingController subclass (final class SignInViewController: UIHostingController<SignInView> {}), and is presented modally, full screen, when sign in is necessary.
Everything is working fine, except I can't figure out how to dismiss the SignInViewController from the SignInView. I have tried adding:
@Environment(\.isPresented) var isPresented
in SignInView and assigning it to false when sign in is successful, but this doesn't appear to interop with UIKit. How can I dismiss the view?
Solution 1:[1]
I found another approach that seems to work well and which feels a little cleaner than some of the other approaches. Steps:
- Add a
dismissActionproperty to the SwiftUI view:
struct SettingsUIView: View {
var dismissAction: (() -> Void)
...
}
- Call the
dismissActionwhen you want to dismiss the view:
Button(action: dismissAction ) {
Text("Done")
}
- When you present the view, provide it with a dismissal handler:
let settingsView = SettingsUIView(dismissAction: {self.dismiss( animated: true, completion: nil )})
let settingsViewController = UIHostingController(rootView: settingsView )
present( settingsViewController, animated: true )
Solution 2:[2]
All the provided answers here didn't work for me, probably because of some weak reference. This is the solution I came up with:
Creating the view and UIHostingController:
let delegate = SheetDismisserProtocol()
let signInView = SignInView(delegate: delegate)
let host = UIHostingController(rootView: AnyView(signInView))
delegate.host = host
// Present the host modally
SheetDismisserProtocol:
class SheetDismisserProtocol: ObservableObject {
weak var host: UIHostingController<AnyView>? = nil
func dismiss() {
host?.dismiss(animated: true)
}
}
The view that has to be dismissed:
struct SignInView: View {
@ObservedObject var delegate: SheetDismisserProtocol
var body: some View {
Button(action: {
self.delegate.dismiss()
})
}
}
Solution 3:[3]
You could just use notifications.
Swift 5.1
In the SwiftUI button handler:
NotificationCenter.default.post(name: NSNotification.Name("dismissSwiftUI"), object: nil)
In the UIKit view controller:
NotificationCenter.default.addObserver(forName: NSNotification.Name("dismissSwiftUI"), object: nil, queue: nil) { (_) in
hostingVC.dismiss(animated: true, completion: nil)
}
Solution 4:[4]
Another approach (relatively easier in my opinion) would be to have an optional property type of UIViewController in your SwiftUI view and then set it to the viewController that will present UIHostingController which will be wrapping your SwiftUI view.
A simple SettingsView:
struct SettingsView: View {
var presentingVC: UIViewController?
var body: some View {
Button(action: {
self.presentingVC?.presentedViewController?.dismiss(animated: true)
}) {
Text("Dismiss")
}
}
}
Then when you present this view from a view controller using UIHostingController:
class ViewController: UIViewController {
private func presentSettingsView() {
var view = SettingsView()
view.presentingVC = self
let hostingVC = UIHostingController(rootView: view)
present(hostingVC, animated: true, completion: nil)
}
}
Now as you can see in the action of the Button in SettingsView, we are going to talk to ViewController to dismiss the view controller it is presenting, which in our case will be the UIHostingController that wraps SettingsView.
Solution 5:[5]
What about extend environment values with hosting controller presenter? It allows to be used like presentationMode, from any view in the hierarchy and it is easily reusable and scalable.
Define your new environment value:
struct UIHostingControllerPresenter {
init(_ hostingControllerPresenter: UIViewController) {
self.hostingControllerPresenter = hostingControllerPresenter
}
private unowned var hostingControllerPresenter: UIViewController
func dismiss() {
if let presentedViewController = hostingControllerPresenter.presentedViewController, !presentedViewController.isBeingDismissed { // otherwise an ancestor dismisses hostingControllerPresenter - which we don't want.
hostingControllerPresenter.dismiss(animated: true, completion: nil)
}
}
}
private enum UIHostingControllerPresenterEnvironmentKey: EnvironmentKey {
static let defaultValue: UIHostingControllerPresenter? = nil
}
extension EnvironmentValues {
/// An environment value that attempts to extend `presentationMode` for case where
/// view is presented via `UIHostingController` so dismissal through
/// `presentationMode` doesn't work.
var uiHostingControllerPresenter: UIHostingControllerPresenter? {
get { self[UIHostingControllerPresenterEnvironmentKey.self] }
set { self[UIHostingControllerPresenterEnvironmentKey.self] = newValue }
}
}
Then pass the value when needed like:
let view = AnySwiftUIView().environment(\.uiHostingControllerPresenter, UIHostingControllerPresenter(self))
let viewController = UIHostingController(rootView: view)
present(viewController, animated: true, completion: nil)
...
And enjoy using
@Environment(\.uiHostingControllerPresenter) private var uiHostingControllerPresenter
...
uiHostingControllerPresenter?.dismiss()
where you otherwise go with
@Environment(\.presentationMode) private var presentationMode
...
presentationMode.wrappedValue.dismiss() // .isPresented = false
Solution 6:[6]
let rootView = SignInView();
let ctrl = UIHostingController(rootView: rootView);
ctrl.rootView.dismiss = {
ctrl.dismiss(animated: true)
}
present(ctrl, animated:true, completion:nil);
pay attention: ctrl.rootView.dismiss not rootView.dismiss
Solution 7:[7]
I had the same problem, and thanks to this post, I could write a mixed solution, to improve usability of the solutions of this post :
final class RootViewController<Content: View>: UIHostingController<AnyView> {
init(rootView: Content) {
let dismisser = ControllerDismisser()
let view = rootView
.environmentObject(dismisser)
super.init(rootView: AnyView(view))
dismisser.host = self
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
final class ControllerDismisser: ObservableObject {
var host: UIHostingController<AnyView>?
func dismiss() {
host?.dismiss(animated: true)
}
}
This way, I can just initialize this controller as a normal UIHostingController
let screen = RootViewController(rootView: MyView())
Note : I used an .environmentObject to pass the object to my views that needed it. This way no need to put it in the initializer, or pass it through all the view hierarchy
Solution 8:[8]
This was a bug in Xcode 12 (and most probably earlier versions of Xcode also). It has been resolved in Xcode 13.0 beta 5 and hopefully will continue to be resolved in the stable release of Xcode 13.0. That said, if you're able to build with Xcode 13 and target iOS 15 (or higher), then prefer the EnvironmentValues.dismiss property over the deprecated EnvironmentValues.presentationMode property, as follows:
struct MyView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
Button("Dismiss") { dismiss() }
}
}
If you're not able to build with Xcode 13 and target iOS 15, then opt for one of the workarounds proposed in this thread.
Solution 9:[9]
I'm not sure whether isPresented will be connected to View's UIHostingController in a future version. You should submit feedback about it.
In the meantime, see this answer for how to access a UIViewController from your Views.
Then, you can just do self.viewController?.dismiss(...).
Solution 10:[10]
I had a similar issue presenting an instance of UIDocumentPickerViewController.
In this scenario, the UIDocumentPickerViewController is presented modally (sheet), which slightly differs from yours -- but the approach may work for you as well.
I could make it work by conforming to the UIViewControllerRepresentable protocol and adding a callback to dismiss the View Controller inside the Coordinator.
Code example:
SwiftUI Beta 5
struct ContentProviderButton: View {
@State private var isPresented = false
var body: some View {
Button(action: {
self.isPresented = true
}) {
Image(systemName: "folder").scaledToFit()
}.sheet(isPresented: $isPresented) { () -> DocumentPickerViewController in
DocumentPickerViewController.init(onDismiss: {
self.isPresented = false
})
}
}
}
/// Wrapper around the `UIDocumentPickerViewController`.
struct DocumentPickerViewController {
private let supportedTypes: [String] = ["public.image"]
// Callback to be executed when users close the document picker.
private let onDismiss: () -> Void
init(onDismiss: @escaping () -> Void) {
self.onDismiss = onDismiss
}
}
// MARK: - UIViewControllerRepresentable
extension DocumentPickerViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = UIDocumentPickerViewController
func makeUIViewController(context: Context) -> DocumentPickerViewController.UIViewControllerType {
let documentPickerController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
documentPickerController.allowsMultipleSelection = true
documentPickerController.delegate = context.coordinator
return documentPickerController
}
func updateUIViewController(_ uiViewController: DocumentPickerViewController.UIViewControllerType, context: Context) {}
// MARK: Coordinator
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var parent: DocumentPickerViewController
init(_ documentPickerController: DocumentPickerViewController) {
parent = documentPickerController
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
// TODO: handle user selection
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
parent.onDismiss()
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Sean McMains |
| Solution 2 | |
| Solution 3 | Jayden Irwin |
| Solution 4 | emrepun |
| Solution 5 | |
| Solution 6 | john07 |
| Solution 7 | zarghol |
| Solution 8 | |
| Solution 9 | |
| Solution 10 | backslash-f |
