你如何在 UIKit 视图控制器和它呈现的 SwiftUI 视图之间共享数据模型?

Posted

技术标签:

【中文标题】你如何在 UIKit 视图控制器和它呈现的 SwiftUI 视图之间共享数据模型?【英文标题】:How do you share a data model between a UIKit view controller and a SwiftUI view that it presents? 【发布时间】:2020-09-08 11:47:35 【问题描述】:

我的数据模型属性在我的表视图控制器中声明,SwiftUI 视图以模态方式呈现。我想要提供的Form 输入来操作数据模型。我在数据流上找到的资源只是在 SwiftUI 视图之间,而我在 UIKit 集成上找到的资源是关于将 UIKit 嵌入到 SwiftUI 中,而不是相反。

此外,对于值类型(在我的例子中是结构)数据模型是否有一种好的方法,或者是否值得将其重新建模为一个类以便它成为一个引用类型?

【问题讨论】:

使它成为 ObservableObject 类并在这里和那里通过引用传递。 @Asperi 既然 SwiftUI 控件采用双向绑定,那么更新该对象将如何工作? 使用 ObservableObject,它仍然只是对传递给 SwiftUI 视图的模型的引用。您仍然需要区分更改并相应地更新表格视图单元格,可能使用 KVO 或 Combine 或任何适合您的架构的东西。至于绑定,您可以在模型本身上创建 Binding<...> 类型的其他属性,并使用其允许您传递自定义 get 和 set 函数的初始化程序,您可以拥有一个使用实际属性的绑定。 您能否提供您的代码以与您想在哪里传递的解释集成在一起? 你可能想看看这个***.com/questions/58442827/… 【参考方案1】:

我们来分析一下……

我的数据模型属性在我的表视图控制器中声明,SwiftUI 视图以模态方式呈现。

这就是你现在所拥有的(可能是简化的)

struct DataModel 
   var value: String


class ViewController: UIViewController 
    var dataModel: DataModel

    // ... some other code

    func showForm() 
       let formView = FormView()
       let controller = UIHostingController(rootView: formView)
       self.present(controller, animating: true)
    

我想要显示的表单输入来操作数据模型。

这里是上面的更新,其中包含将值类型数据传递到 SwiftUI 视图的简单演示,并在无需对 UIKit 部分进行任何重构的情况下将其更新/修改/处理。

这个想法很简单——你通过值将当前模型传递给 SwiftUI,并在完成回调中将其返回并更新并应用于本地属性(因此,如果设置了任何观察者,它们都会按预期工作)

使用 Xcode 12 / ios 14 测试。

class ViewController: UIViewController 
    var dataModel: DataModel

    // ... some other code

    func showForm() 
        let formView = FormView(data: self.dataModel)  [weak self] newData in
                self?.dismiss(animated: true) 
                self?.dataModel = newData
            
        

        let controller = UIHostingController(rootView: formView)
        self.present(controller, animated: true)
    


struct FormView: View 
    @State private var data: DataModel
    private var completion: (DataModel) -> Void

    init(data: DataModel, completion: @escaping (DataModel) -> Void) 
        self._data = State(initialValue: data)
        self.completion = completion
    

    var body: some View 
        Form 
            TextField("", text: $data.value)
            Button("Done") 
                completion(data)
            
        
    

【讨论】:

【参考方案2】:

在组织 UI 代码时,最佳实践要求包含 3 个部分:

    视图(仅视觉结构、样式、动画) 模型(您的数据和业务逻辑) 连接视图和模型的秘诀

在 UIKit 中,我们使用MVP 方法,其中 UIViewController 子类通常代表秘密酱汁部分。

在 SwiftUI 中,由于提供了数据绑定工具,使用 MVVM 方法更容易。在 MVVM 中,“ViewModel”是秘诀。它是一个自定义结构,用于保存模型数据以供您的视图呈现,在模型数据更新时触发视图更新,并转发 UI 操作以对您的模型执行某些操作。

例如,编辑名称的表单可能如下所示:

struct MyForm: View 
    let viewModel: MyFormViewModel
    var body: some View 
        Form 
            TextField("Name", text: $viewModel.name)
            Button("Submit", action:  self.viewModel.submit() )
        
    


class MyFormViewModel 
    var name: String // implement a custom setter if needed
    init(name: String)  this.name = name 
    func submit() 
        print("submitting: \(name)")
    

有了这个,很容易将 UI 动作转发到 UIKit 控制器。一种标准方法是使用委托协议:

protocol MyFormViewModelDelegate: class 
    func didSubmit(viewModel: MyFormViewModel)


class MyFormViewModel 
    weak var delegate: MyFormViewModelDelegate?
    func submit() 
        self.delegate?.didSubmit(viewModel: self)
    
    ...

最后,你的 UIViewController 可以实现 MyFormViewModelDelegate,创建一个 MyFormViewModel 实例,并通过将 self 设置为 delegate 来订阅它,然后将 MyFormViewModel 对象传递给 MyForm 视图。

改进和其他提示:

    如果这对您来说太老套了,您可以使用 Combine 而不是委托来订阅/发布 didSubmit 事件。 在这个简单的示例中,模型只是一个字符串。随意使用您的自定义模型数据类型。 无法保证 MyFormViewModel 对象在视图被销毁时仍保持活动状态,因此如果您希望在某处保持强引用更长时间,则可能是明智之举。 $viewModel.name 语法是一种魔术,它创建一个引用 MyFormViewModel 的可变 name 属性的 Binding<String> 实例。

【讨论】:

我也是这样做的,效果很好。组合也会很棒,但这对我来说意味着很多重构。有什么显着优势吗?我的意思是,对于协议和委托,我真的没有什么不能做的......

以上是关于你如何在 UIKit 视图控制器和它呈现的 SwiftUI 视图之间共享数据模型?的主要内容,如果未能解决你的问题,请参考以下文章

如何知道 UIKit 视图控制器中的 swiftUI 事件? ||如何提供 swiftUI 和 uikit 之间的通信

自定义Controller转场动画

如何从 SwiftUI 视图转到 UIKit TabViewController?

如何在不隐藏 tabBar 的情况下呈现视图控制器

UIAlertController通过模态视图控制器向上和向下推动

如何让 UIKit 为我提供自定义转换的转换协调器?