Dismiss a SwiftUI View that is contained in a UIHostingController

混江龙づ霸主 提交于 2021-01-20 18:56:20

问题


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?


回答1:


I ended up finding a much simpler solution than what was offered:


final class SettingsViewController: UIHostingController<SettingsView> {
    required init?(coder: NSCoder) {
        super.init(coder: coder, rootView: SettingsView())
        rootView.dismiss = dismiss
    }

    func dismiss() {
        dismiss(animated: true, completion: nil)
    }
}

struct SettingsView: View {
    var dismiss: (() -> Void)?

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Button("Dimiss", action: dismiss!)
                }
            }
            .navigationBarTitle("Settings")
        }
    }
}



回答2:


I found another approach that seems to work well and which feels a little cleaner than some of the other approaches. Steps:

  1. Add a dismissAction property to the SwiftUI view:
struct SettingsUIView: View {
    var dismissAction: (() -> Void)
    ...
}    
  1. Call the dismissAction when you want to dismiss the view:
Button(action: dismissAction ) {
    Text("Done")
}
  1. 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 )



回答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)
}



回答4:


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()
        })
    }
}



回答5:


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




回答6:


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.




回答7:


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(...).




回答8:


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()
        }
    }
}



回答9:


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() {
        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


来源:https://stackoverflow.com/questions/57190511/dismiss-a-swiftui-view-that-is-contained-in-a-uihostingcontroller

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!