关闭包含在 UIHostingController 中的 SwiftUI 视图
Dismiss a SwiftUI View that is contained in a UIHostingController
我已将我的登录视图控制器重写为 SwiftUI View
。 SignInView
包含在 UIHostingController
子类 (final class SignInViewController: UIHostingController<SignInView> {}
) 中,并在需要登录时以全屏模式呈现。
一切正常,除了我不知道如何从 SignInView
中删除 SignInViewController
。我尝试添加:
@Environment(\.isPresented) var isPresented
in SignInView
并在登录成功时将其分配给 false
,但这似乎无法与 UIKit 互操作。我怎样才能关闭视图?
我不确定 isPresented
是否会在未来的版本中连接到 View
的 UIHostingController
。你应该 submit feedback 了解一下。
同时,请参阅 了解如何从您的 View
访问 UIViewController。
然后,你可以self.viewController?.dismiss(...)
。
我在呈现 UIDocumentPickerViewController
.
的实例时遇到了类似的问题
在这种情况下,UIDocumentPickerViewController 以模态方式呈现 (sheet
),这与您的稍有不同 -- 但该方法也可能适用于您。
我可以通过遵守 UIViewControllerRepresentable
协议并添加一个回调来关闭 Coordinator
.
中的视图控制器来使其工作
代码示例:
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()
}
}
}
更新: 来自 iOS 15 beta 1 的发行说明:
isPresented, PresentationMode, and the new DismissAction action dismiss a hosting controller presented from UIKit. (52556186)
我最终找到了一个比所提供的解决方案简单得多的解决方案:
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")
}
}
}
您可以只使用通知。
Swift 5.1
在 SwiftUI 按钮处理程序中:
NotificationCenter.default.post(name: NSNotification.Name("dismissSwiftUI"), object: nil)
在UI套件视图控制器中:
NotificationCenter.default.addObserver(forName: NSNotification.Name("dismissSwiftUI"), object: nil, queue: nil) { (_) in
hostingVC.dismiss(animated: true, completion: nil)
}
这里提供的所有答案都不适合我,可能是因为某些参考资料不充分。这是我想出的解决方案:
创建视图和 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)
}
}
不得不驳回的观点:
struct SignInView: View {
@ObservedObject var delegate: SheetDismisserProtocol
var body: some View {
Button(action: {
self.delegate.dismiss()
})
}
}
我发现另一种方法似乎很有效,而且感觉比其他一些方法更简洁。步骤:
- 添加一个
dismissAction
属性 到 SwiftUI 视图:
struct SettingsUIView: View {
var dismissAction: (() -> Void)
...
}
- 当您想要关闭视图时调用
dismissAction
:
Button(action: dismissAction ) {
Text("Done")
}
- 当您呈现视图时,为其提供解雇处理程序:
let settingsView = SettingsUIView(dismissAction: {self.dismiss( animated: true, completion: nil )})
let settingsViewController = UIHostingController(rootView: settingsView )
present( settingsViewController, animated: true )
我遇到了同样的问题,感谢这个 post,我可以编写一个混合解决方案,以提高这个 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)
}
}
这样,我就可以将这个控制器初始化为普通的 UIHostingController
let screen = RootViewController(rootView: MyView())
注意 :我使用 .environmentObject
将对象传递给我需要它的视图。这样就不需要把它放在初始化器中,或者通过所有的视图层次结构传递它
另一种方法(在我看来相对容易)是在 SwiftUI
view
中有一个可选的 属性 类型的 UIViewController
然后将其设置为viewController 将显示 UIHostingController
,它将包装您的 SwiftUI
视图。
一个简单的设置视图:
struct SettingsView: View {
var presentingVC: UIViewController?
var body: some View {
Button(action: {
self.presentingVC?.presentedViewController?.dismiss(animated: true)
}) {
Text("Dismiss")
}
}
}
然后当您使用 UIHostingController
从视图控制器呈现此视图时:
class ViewController: UIViewController {
private func presentSettingsView() {
var view = SettingsView()
view.presentingVC = self
let hostingVC = UIHostingController(rootView: view)
present(hostingVC, animated: true, completion: nil)
}
}
现在,正如您在 SettingsView
中 Button
的操作中看到的那样,我们将与 ViewController
对话以关闭它正在呈现的视图控制器,这在我们的case 将是包装 SettingsView
.
的 UIHostingController
如何使用托管控制器演示器扩展环境值?它允许从层次结构的任何视图中像 presentationMode
一样使用,并且很容易重用和扩展。
定义您的新环境值:
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 }
}
}
然后在需要时传递值,例如:
let view = AnySwiftUIView().environment(\.uiHostingControllerPresenter, UIHostingControllerPresenter(self))
let viewController = UIHostingController(rootView: view)
present(viewController, animated: true, completion: nil)
...
享受使用
@Environment(\.uiHostingControllerPresenter) private var uiHostingControllerPresenter
...
uiHostingControllerPresenter?.dismiss()
你去哪里
@Environment(\.presentationMode) private var presentationMode
...
presentationMode.wrappedValue.dismiss() // .isPresented = false
let rootView = SignInView();
let ctrl = UIHostingController(rootView: rootView);
ctrl.rootView.dismiss = {
ctrl.dismiss(animated: true)
}
present(ctrl, animated:true, completion:nil);
注意:ctrl.rootView.dismiss不是rootView.dismiss
这是 Xcode 12 中的错误(很可能也是 Xcode 的早期版本)。它已在 Xcode 13.0 beta 5 中得到解决,并希望在 Xcode 13.0 的稳定版本中继续得到解决。也就是说,如果您能够使用 Xcode 13 和目标 iOS 15(或更高)进行构建,那么更喜欢 EnvironmentValues.dismiss property over the deprecated EnvironmentValues.presentationMode 属性,如下所示:
struct MyView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
Button("Dismiss") { dismiss() }
}
}
如果您无法使用 Xcode 13 和目标 iOS 15 进行构建,请选择此线程中提出的解决方法之一。
我已将我的登录视图控制器重写为 SwiftUI View
。 SignInView
包含在 UIHostingController
子类 (final class SignInViewController: UIHostingController<SignInView> {}
) 中,并在需要登录时以全屏模式呈现。
一切正常,除了我不知道如何从 SignInView
中删除 SignInViewController
。我尝试添加:
@Environment(\.isPresented) var isPresented
in SignInView
并在登录成功时将其分配给 false
,但这似乎无法与 UIKit 互操作。我怎样才能关闭视图?
我不确定 isPresented
是否会在未来的版本中连接到 View
的 UIHostingController
。你应该 submit feedback 了解一下。
同时,请参阅 View
访问 UIViewController。
然后,你可以self.viewController?.dismiss(...)
。
我在呈现 UIDocumentPickerViewController
.
在这种情况下,UIDocumentPickerViewController 以模态方式呈现 (sheet
),这与您的稍有不同 -- 但该方法也可能适用于您。
我可以通过遵守 UIViewControllerRepresentable
协议并添加一个回调来关闭 Coordinator
.
代码示例:
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()
}
}
}
更新: 来自 iOS 15 beta 1 的发行说明:
isPresented, PresentationMode, and the new DismissAction action dismiss a hosting controller presented from UIKit. (52556186)
我最终找到了一个比所提供的解决方案简单得多的解决方案:
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")
}
}
}
您可以只使用通知。
Swift 5.1
在 SwiftUI 按钮处理程序中:
NotificationCenter.default.post(name: NSNotification.Name("dismissSwiftUI"), object: nil)
在UI套件视图控制器中:
NotificationCenter.default.addObserver(forName: NSNotification.Name("dismissSwiftUI"), object: nil, queue: nil) { (_) in
hostingVC.dismiss(animated: true, completion: nil)
}
这里提供的所有答案都不适合我,可能是因为某些参考资料不充分。这是我想出的解决方案:
创建视图和 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)
}
}
不得不驳回的观点:
struct SignInView: View {
@ObservedObject var delegate: SheetDismisserProtocol
var body: some View {
Button(action: {
self.delegate.dismiss()
})
}
}
我发现另一种方法似乎很有效,而且感觉比其他一些方法更简洁。步骤:
- 添加一个
dismissAction
属性 到 SwiftUI 视图:
struct SettingsUIView: View {
var dismissAction: (() -> Void)
...
}
- 当您想要关闭视图时调用
dismissAction
:
Button(action: dismissAction ) {
Text("Done")
}
- 当您呈现视图时,为其提供解雇处理程序:
let settingsView = SettingsUIView(dismissAction: {self.dismiss( animated: true, completion: nil )})
let settingsViewController = UIHostingController(rootView: settingsView )
present( settingsViewController, animated: true )
我遇到了同样的问题,感谢这个 post,我可以编写一个混合解决方案,以提高这个 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)
}
}
这样,我就可以将这个控制器初始化为普通的 UIHostingController
let screen = RootViewController(rootView: MyView())
注意 :我使用 .environmentObject
将对象传递给我需要它的视图。这样就不需要把它放在初始化器中,或者通过所有的视图层次结构传递它
另一种方法(在我看来相对容易)是在 SwiftUI
view
中有一个可选的 属性 类型的 UIViewController
然后将其设置为viewController 将显示 UIHostingController
,它将包装您的 SwiftUI
视图。
一个简单的设置视图:
struct SettingsView: View {
var presentingVC: UIViewController?
var body: some View {
Button(action: {
self.presentingVC?.presentedViewController?.dismiss(animated: true)
}) {
Text("Dismiss")
}
}
}
然后当您使用 UIHostingController
从视图控制器呈现此视图时:
class ViewController: UIViewController {
private func presentSettingsView() {
var view = SettingsView()
view.presentingVC = self
let hostingVC = UIHostingController(rootView: view)
present(hostingVC, animated: true, completion: nil)
}
}
现在,正如您在 SettingsView
中 Button
的操作中看到的那样,我们将与 ViewController
对话以关闭它正在呈现的视图控制器,这在我们的case 将是包装 SettingsView
.
UIHostingController
如何使用托管控制器演示器扩展环境值?它允许从层次结构的任何视图中像 presentationMode
一样使用,并且很容易重用和扩展。
定义您的新环境值:
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 }
}
}
然后在需要时传递值,例如:
let view = AnySwiftUIView().environment(\.uiHostingControllerPresenter, UIHostingControllerPresenter(self))
let viewController = UIHostingController(rootView: view)
present(viewController, animated: true, completion: nil)
...
享受使用
@Environment(\.uiHostingControllerPresenter) private var uiHostingControllerPresenter
...
uiHostingControllerPresenter?.dismiss()
你去哪里
@Environment(\.presentationMode) private var presentationMode
...
presentationMode.wrappedValue.dismiss() // .isPresented = false
let rootView = SignInView();
let ctrl = UIHostingController(rootView: rootView);
ctrl.rootView.dismiss = {
ctrl.dismiss(animated: true)
}
present(ctrl, animated:true, completion:nil);
注意:ctrl.rootView.dismiss不是rootView.dismiss
这是 Xcode 12 中的错误(很可能也是 Xcode 的早期版本)。它已在 Xcode 13.0 beta 5 中得到解决,并希望在 Xcode 13.0 的稳定版本中继续得到解决。也就是说,如果您能够使用 Xcode 13 和目标 iOS 15(或更高)进行构建,那么更喜欢 EnvironmentValues.dismiss property over the deprecated EnvironmentValues.presentationMode 属性,如下所示:
struct MyView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
Button("Dismiss") { dismiss() }
}
}
如果您无法使用 Xcode 13 和目标 iOS 15 进行构建,请选择此线程中提出的解决方法之一。