将 MVC 之类的设计模式与 SwiftUI 结合使用

Posted

技术标签:

【中文标题】将 MVC 之类的设计模式与 SwiftUI 结合使用【英文标题】:Using a design pattern like MVC with SwiftUI 【发布时间】:2020-10-18 17:00:58 【问题描述】:

我正在尝试实现类似 MVC 的设计模式,以实现代码不同部分之间的低耦合。网上很少有关于 ios 或 swift UI 开发和 MVC 模式的资料我个人认为没有帮助。

我想了解的是控制器类应该如何在 Swift UI 中控制或渲染 UI?

例如遵循 MVC 模式 - 视图不应该知道模型的外观,因此将对象从数据库发送回视图以直观地呈现它不是一个好主意。

假设我们有下面的View和Controller,当从DB发回数据时,我应该如何处理控制器和视图之间的交互,以便在视图中直观地呈现它?

查看:

import SwiftUI
import Foundation

struct SwiftUIView: View 

    
    var assignmentController = AssignmentController()
    

    @State var assignmentName : String = ""
    @State var notes : String = ""
  

  
    var body: some View 
        
        NavigationView 
            VStack 
                Form 
                   
                        
                        TextField("Assignment Name", text: $assignmentName)

    
                        TextField("Notes", text: $notes)
                
     
            
                
                Button(action: 
                                                               
                    self.assignmentController.retrieveFirstAssignment()
                                       ) 
                                                                                                                                                Text("Get The First Assignment !")
                                                                                    

            
            .navigationBarTitle("First Assignment")
        
        
    


控制器

var assignmentModel = AssignmentModel()

func retrieveFirstAssignment()

    var firstAssignment : Assignment

    firstAssignment=assignmentModel.retrieveFirstAssignment()
    

目前,它对找到的对象什么都不做。

型号

模型中的一个对象由两个 String 字段组成:“assignmentName”和“notes”。

*我们假设分配模型有一个工作函数,它从数据库中检索一个任务,以便在视图中呈现它。

【问题讨论】:

谢谢。这正是我的问题,您应该如何与视图进行交互,因为视图不应该知道模型的外观。这就是为什么我暂时不使用它的原因。 当然可以使用几乎任何你想要的设计模式,但你考虑过 MVVM 吗?它似乎比 MVC 和 Apple 在他们的教程系列中自己采用它更适合 SwiftUI 做事。 我也对 MVVM 持开放态度,但在这种情况下,我的问题仍然是如何在 View Model 和 View 之间建立适当的绑定连接。 ObservableObject/ObservedObject. 如果有人可以使用我使用的演示类(用视图模型替换控制器)提供基本代码示例,将不胜感激 【参考方案1】:
struct SwiftUIView: View 

    @State m = AssignmentModel()
   
    var body: some View 
        // use m   
    
    func loadAssignmentFromDB() 
        m.retrieveFirstAssignment()
    

这就是我所说的“带有内置 MVVM 的增强型 MVC”。

它可以满足您对 MVC 和可能的 MVVM 的追求,同时省力。

现在我将讨论原因:

    function,或者更具体地说,mutation是Control。您不需要一个名为“Controller”的对象来在 MVC 中使用 C。否则我们还不如再坚持 UIKit 10 年。

当您的模型是值类型时,这很有意义。没有你的具体说法,没有什么可以改变它,例如; @State 注释。由于改变模型的唯一方法是通过这些指定的端点,因此您的 Control 仅在改变这些端点的函数中生效。

    引用另一个回复:

确实,SwiftUI 与 MVVM 比与 MVC 更接近。然而,Apple 文档中的几乎所有示例代码都非常简单,以至于完全忽略了 ViewModel(和/或 MVC 中的控制器)。一旦你开始创建更大的项目,就需要一些东西来连接你的视图和模型。但是,IMO,SwiftUI 文档并没有(还)以令人满意的方式完全解决这个问题。

SwiftUI,如果有的话,增强了 MVC。拥有 ViewModel 的目的是什么?

a.) 拥有模型视图绑定,它出现在我上面的代码 sn-p 中。

b.) 管理与对象关联的状态。如果@State 没有给您留下由您管理状态的印象,那么我不知道会发生什么。有趣的是,有多少 MVVM 开发人员对此视而不见。就像你不需要 View Controller 来控制一样,你不需要 ViewModel 来做 VM。设计模式是一种@State 的思想。与特定的命名和严格的结构无关。

c.) 说我对 MVVM 持开放态度,但没有根据。您认为哪个代码 sn-p 更有机会扩展到更大的项目?我的紧凑型还是另一条回复中建议的? 提示:想想有多少额外的文件、视图模型、observableobjects、粘合代码、pass-around-vm-as-parameters、init 函数接受 vm 作为参数,你将拥有。这些只是供您在另一个对象中编写一些代码。它没有提到减少或简化手头的任务。地狱它甚至告诉你如何重构你的控制代码,所以你很可能只是重复你在 MVC 中做错的任何事情。我有没有提到 ViewModel 是一个具有隐式状态管理的共享引用类型对象?那么当你打算用一个引用类型模型覆盖它时,拥有一个 value 类型模型有什么意义呢?

MVVM 开发人员说 SwiftUI 的基本形式无法扩展到更大的项目,这很有趣。保持简单是扩展的唯一方法。

这是我观察到的 2020 年开发进展路线图。 第1天:初学者 Day2:google了一些,放下MVC Day3:google 更多,SwiftUI 不可扩展 Day4:好的,我需要MVVM+RxSwift+Coordinator+Router+DependencyInjection来避免SDK的缺点。

由于这似乎是一个常见的初学者问题,我的建议是在跑步之前先学会走路。

我亲眼看到 RxSwift 开发人员将控制器代码移动到视图中,使控制器看起来“干净”,并且需要 3 个第三方库(一个是自定义 fork)来发送 http GET。

如果你不能让简单的事情变得简单,那么设计模式就毫无意义。

【讨论】:

【参考方案2】:

对我来说,这是一个很好的问题。的确,SwiftUI 与 MVVM 比与 MVC 更接近。然而,Apple 文档中的几乎所有示例代码都非常简单,以至于完全忽略了 ViewModel(和/或 MVC 中的控制器)。一旦你开始创建更大的项目,就需要一些东西来连接你的视图和模型。然而,IMO,SwiftUI 文档并没有(还)以令人满意的方式完全解决这个问题。我希望其他开发人员纠正我或对此进行扩展(我仍在学习),但这是我迄今为止发现的:

为了在非示例项目中管理更新视图,您几乎总是希望使用 ObservableObject/ObservedObject。 视图应该只在对象发生变化时需要更新时才观察它。最好将更新委托给子视图。 创建一个大的 ObservableObject 并为其所有属性添加@Published 可能很诱人。但是,这意味着即使视图甚至不依赖的属性发生变化,观察该对象的视图也会更新(有时是可见的)。 绑定是表示可以修改数据的控件的视图最自然的接口。请注意,绑定不会触发更新视图。更新视图应该由@State 或@ObservedObject 管理(这可以由控件的父视图完成)。 常量是视图的自然界面,只显示数据(而不是修改数据)。

以下是我将其应用于您的示例的方法:

import SwiftUI

//
// Helper class for observing value types
//
class ObservableValue<Value: Hashable>: ObservableObject 
    @Published var value: Value
    
    init(initialValue: Value) 
        value = initialValue
    


//
// Model
//
struct Assignment 
    let name : String
    let notes: String


//
// ViewModel?
//
// Usually a view model transforms data so it is usable by the view. Strings are already
// usable in our components. The only change here is to wrap the strings in an
// ObservableValue so views can listen for changes to the individual properties.
//
// Note: In Swift you often see transformations of the data implemented as extensions to
// the model rather than in a separate ViewModel.

class AssignmentModelView 
    var name : ObservableValue<String>
    var notes: ObservableValue<String>
    
    init(assignment: Assignment) 
        name  = ObservableValue<String>(initialValue: assignment.name)
        notes = ObservableValue<String>(initialValue: assignment.notes)
    
    
    var assignment: Assignment 
        Assignment(name: name.value, notes: notes.value)
    


//
// Controller
//
// Publish the first assignment so Views depending on it can update whenever we change
// the first assignment (**not** update its properties)
class AssignmentController: ObservableObject 
    @Published var firstAssignment: AssignmentModelView?

    func retrieveFirstAssignment() 
        let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
        
        firstAssignment = AssignmentModelView(assignment: assignment)
    


struct ContentView: View 

    // In a real app you should use dependency injection here
    // (i.e. provide the assignmentController as a parameter)
    @ObservedObject var assignmentController = AssignmentController()
  
    var body: some View 
        
        NavigationView 
            VStack 
            
                // I prefer to use `map` instead of conditional views, since it
                // eliminates the need for forced unwrapping
                self.assignmentController.firstAssignment.map  assignmentModelView in
                    Form 
                        ObservingTextField(title: "Assignment Name", value:  assignmentModelView.name)
                        ObservingTextField(title: "Notes", value: assignmentModelView.notes)
                    
                
               
                Button(action:  self.retrieveFirstAssignment() ) 
                    Text("Get The First Assignment !")
                
            
            .navigationBarTitle("First Assignment")
        
    
    
    func retrieveFirstAssignment() 
        assignmentController.retrieveFirstAssignment()
    


//
// Wrapper for TextField that correctly updates whenever the value
// changes
//
struct ObservingTextField: View 
    let title: String
    @ObservedObject var value: ObservableValue<String>
    
    var body: some View 
        TextField(title, text: $value.value)
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    


这对您的应用来说可能是多余的。有一个更直接的版本,但它有 缺点是 TextFields 会更新,即使它们的内容没有改变。在 这个特殊的例子我认为这并不重要。对于较大的项目,它可能 变得重要,而不是(仅)出于性能原因,但更新有时非常重要 可见的。供参考:这是更简单的版本。

import SwiftUI

// Model
struct Assignment 
    let name : String
    let notes: String


// ViewModel
class AssignmentViewModel: ObservableObject 
    @Published var name : String
    @Published var notes: String
    
    init(assignment: Assignment) 
        name  = assignment.name
        notes = assignment.notes
    


// Controller
class AssignmentController: ObservableObject 
    @Published var firstAssignment: AssignmentViewModel?

    func retrieveFirstAssignment() 
        let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
        
        firstAssignment = AssignmentViewModel(assignment: assignment)
    


struct ContentView: View 
    // In a real app you should use dependency injection here
    // (i.e. provide the assignmentController as a parameter)
    @ObservedObject var assignmentController = AssignmentController()
  
    var body: some View 
        
        NavigationView 
            VStack 
                self.assignmentController.firstAssignment.map  assignmentModelView in
                    FirstAssignmentView(firstAssignment: assignmentModelView)
                
               
                Button(action:  self.retrieveFirstAssignment() ) 
                    Text("Get The First Assignment !")
                
            
                .navigationBarTitle("First Assignment")
        
    
    
    func retrieveFirstAssignment() 
        assignmentController.retrieveFirstAssignment()
    


struct FirstAssignmentView: View 
    @ObservedObject var firstAssignment: AssignmentViewModel
    
    var body: some View 
        Form 
            TextField("Assignment Name", text: $firstAssignment.name)
            TextField("Notes", text: $firstAssignment.notes)
        
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

【讨论】:

【参考方案3】:

我也有同样的问题,我偶然发现了 Matteo Manferdini 的一篇优秀论文,他在其中描述了如何在 SwiftUI 中使用 MVC 模型。 我用他的论文重构了一个使用 CoreData 的Pizza app。即使我还是 SwiftUI 的初学者,它也让我对如何实现 MVC 有了很好的理解。你可以找到 Matteo 的论文here。

【讨论】:

以上是关于将 MVC 之类的设计模式与 SwiftUI 结合使用的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 将几何阅读器与 ForEach 结合使用

SwiftUI:如何将 .toggle() not Toggle(isOn..) 与 UserDefault 结合使用

将 SwiftUI 中的文本与背景属性相结合会产生错误,因为无法将“某些视图”类型的值转换为预期的参数类型“文本”?

iOS SwiftUI 在与代码交互时如何调出“嵌入 VStack”之类的额外操作?

将 ASP.Net MVC 与 WebForms 相结合

将 GZIP 压缩与 Spring Boot/MVC/JavaConfig 与 RESTful 结合使用