问题
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:
- Add a
dismissAction
property to the SwiftUI view:
struct SettingsUIView: View {
var dismissAction: (() -> Void)
...
}
- Call the
dismissAction
when 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 )
回答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 View
s.
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