我已将登录视图控制器重写为 SwiftUI
View
。 SignInView
包含在 UIHostingController
子类 (final class SignInViewController: UIHostingController<SignInView> {}
) 中,并在需要登录时以全屏模式呈现。
一切工作正常,除了我不知道如何从
SignInViewController
中消除 SignInView
。我尝试添加:
@Environment(\.isPresented) var isPresented
in
SignInView
并在登录成功时将其分配给 false
,但这似乎无法与 UIKit 互操作。我怎样才能消除这个观点?
我发现另一种方法似乎效果很好,而且比其他一些方法感觉更干净。步骤:
dismissAction
属性: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 )
更新:来自 iOS 15 beta 1 的发行说明:
isPresented、PresentationMode 和新的 DismissAction 操作会解除 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")
}
}
}
这里提供的所有答案对我来说都不起作用,可能是因为一些弱参考。这是我想出的解决方案:
创建视图和 UIHostingController:
let delegate = SheetDismisserProtocol()
let signInView = SignInView(delegate: delegate)
let host = UIHostingController(rootView: AnyView(signInView))
delegate.host = host
// Present the host modally
SheetDismisser 协议:
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()
})
}
}
另一种方法(在我看来相对更容易)是在您的
UIViewController
SwiftUI
中有一个可选的 view
属性类型,然后将其设置为将呈现 UIHostingController
的 viewController,它将包装您的 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)
}
}
现在,正如您在
Button
中 SettingsView
的操作中看到的,我们将与 ViewController
对话以关闭它所呈现的视图控制器,在我们的例子中,它将是包装 的
UIHostingController
SettingsView
。
您可以只使用通知。
斯威夫特 5.1
在 SwiftUI 按钮处理程序中:
NotificationCenter.default.post(name: NSNotification.Name("dismissSwiftUI"), object: nil)
在 UIKit 视图控制器中:
NotificationCenter.default.addObserver(forName: NSNotification.Name("dismissSwiftUI"), object: nil, queue: nil) { (_) in
hostingVC.dismiss(animated: true, completion: nil)
}
let rootView = SignInView();
let ctrl = UIHostingController(rootView: rootView);
ctrl.rootView.dismiss = {[weak ctrl] in
ctrl?.dismiss(animated: true)
}
present(ctrl, animated:true, completion:nil);
注意:ctrl.rootView.dismiss不是rootView.dismiss
使用托管控制器演示器扩展环境值怎么样?它允许像
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
这是 Xcode 12 中的一个错误(很可能也是 Xcode 的早期版本)。它已在 Xcode 13.0 beta 5 中得到解决,并希望在 Xcode 13.0 的稳定版本中继续得到解决。也就是说,如果您能够使用 Xcode 13 进行构建并面向 iOS 15(或更高版本),那么更喜欢使用 EnvironmentValues.dismiss 属性,而不是已弃用的 EnvironmentValues.presentationMode 属性,如下所示:
struct MyView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
Button("Dismiss") { dismiss() }
}
}
如果您无法使用 Xcode 13 进行构建并以 iOS 15 为目标,请选择本线程中建议的解决方法之一。
我遇到了同样的问题,感谢这篇文章,我可以编写一个混合解决方案,以提高本文解决方案的可用性:
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
将对象传递给需要它的视图。这样就无需将其放入初始化程序中,或将其传递给所有视图层次结构
iOS 15 及以上版本
struct MyView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Hello World")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Dismiss") {
dismiss()
}
}
}
}
}
}
我有一个类似的问题,展示了一个
UIDocumentPickerViewController
的实例。
在这种情况下,UIDocumentPickerViewController 以模态方式呈现(
sheet
),这与您的略有不同 - 但该方法也可能适合您。
我可以通过遵守
UIViewControllerRepresentable
协议并添加回调来关闭 Coordinator
内的视图控制器来使其工作。
代码示例:
SwiftUI 测试版 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()
}
}
}
我相信你可以直接使用环境变量来解雇:
@Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss") {
presentationMode.wrappedValue.dismiss()
}
}
有一种更简单的方法可以做到这一点,至少从 iOS 18 开始:只需将视图设为
NavigationStack
并将其传递给托管控制器即可:
struct MyView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
// ....
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
然后,像这样显示:
let introController = UIHostingController(rootView: MyViw())
parent.present(introController, animated: true, completion: nil)
单击“完成”按钮将关闭您的托管控制器。