关闭 UIHostingController 中包含的 SwiftUI 视图

问题描述 投票:0回答:14

我已将登录视图控制器重写为 SwiftUI

View
SignInView
包含在
UIHostingController
子类 (
final class SignInViewController: UIHostingController<SignInView> {}
) 中,并在需要登录时以全屏模式呈现。

一切工作正常,除了我不知道如何从

SignInViewController
中消除
SignInView
。我尝试添加:

@Environment(\.isPresented) var isPresented

in

SignInView
并在登录成功时将其分配给
false
,但这似乎无法与 UIKit 互操作。我怎样才能消除这个观点?

ios uikit swiftui
14个回答
62
投票

我发现另一种方法似乎效果很好,而且比其他一些方法感觉更干净。步骤:

  1. 向 SwiftUI 视图添加
    dismissAction
    属性:
struct SettingsUIView: View {
    var dismissAction: (() -> Void)
    ...
}    
  1. 当您想要关闭视图时,请调用
    dismissAction
Button(action: dismissAction ) {
    Text("Done")
}
  1. 当您呈现视图时,为其提供一个解除处理程序:
let settingsView = SettingsUIView(dismissAction: {self.dismiss( animated: true, completion: nil )})
let settingsViewController = UIHostingController(rootView: settingsView )

present( settingsViewController, animated: true )

52
投票

更新:来自 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")
        }
    }
}

16
投票

这里提供的所有答案对我来说都不起作用,可能是因为一些弱参考。这是我想出的解决方案:

创建视图和 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()
        })
    }
}

12
投票

另一种方法(在我看来相对更容易)是在您的

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


8
投票

您可以只使用通知。

斯威夫特 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)
}

7
投票
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


6
投票

使用托管控制器演示器扩展环境值怎么样?它允许像

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

5
投票

这是 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 为目标,请选择本线程中建议的解决方法之一。


4
投票

我遇到了同样的问题,感谢这篇文章,我可以编写一个混合解决方案,以提高本文解决方案的可用性:

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
将对象传递给需要它的视图。这样就无需将其放入初始化程序中,或将其传递给所有视图层次结构


3
投票

iOS 15 及以上版本

struct MyView: View {
@Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationView {
            Text("Hello World")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Dismiss") {
                            dismiss()
                        }
                    }
                }
       }
   }
}

1
投票

我不确定未来版本中

isPresented
是否会连接到
View
UIHostingController
。 您应该提交反馈

同时,请参阅这个答案,了解如何从您的

View
访问 UIViewController。

然后,你就可以做

self.viewController?.dismiss(...)


1
投票

我有一个类似的问题,展示了一个

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

1
投票

我相信你可以直接使用环境变量来解雇:

@Environment(\.presentationMode) var presentationMode

var body: some View {
    Button("Dismiss") {
        presentationMode.wrappedValue.dismiss()
    }
}

0
投票

有一种更简单的方法可以做到这一点,至少从 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)

单击“完成”按钮将关闭您的托管控制器。

© www.soinside.com 2019 - 2024. All rights reserved.