将 MVC 等设计模式与 SwiftUI 结合使用

Che*_*rK2 5 model-view-controller design-patterns ios swift swiftui

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

我想了解的是控制器类应该如何控制或呈现 Swift UI 中的 UI?

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

假设我们有以下视图和控制器,当从数据库发送回数据时,我应该如何进行控制器和视图之间的交互,以便将其直观地呈现在视图中?

看法:

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")
        }
        
    
}
}
Run Code Online (Sandbox Code Playgroud)

控制器

var assignmentModel = AssignmentModel()

func retrieveFirstAssignment()
{
    var firstAssignment : Assignment

    firstAssignment=assignmentModel.retrieveFirstAssignment()
    
}
Run Code Online (Sandbox Code Playgroud)

目前,它对找到的对象没有任何作用。

模型

模型中的对象由两个字符串字段组成:“ taskingName ”和“ notes ”。

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

Jim*_*lai 8

struct SwiftUIView: View {

    @State m = AssignmentModel()
   
    var body: some View {
        // use m   
    }
    func loadAssignmentFromDB() {
        m.retrieveFirstAssignment()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

它同时满足了您对 MVC 和可能的 MVVM 的追求,并且花费更少的精力。

现在我将论证原因:

  1. 功能,或更具体地说,突变是控制。你不需要一个名为“Controller”的对象来在 MVC 中使用 C。否则我们还不如再坚持 UIKit 10 年。

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

  1. 引用另一个回复:

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

SwiftUI 增强了 MVC。拥有 ViewModel 的目的是什么?

a.) 具有模型视图绑定,这出现在我上面的代码片段中。

b.) 管理与对象关联的状态。如果@State没有给您留下由您来管理状态的印象,那么我不知道什么会。有趣的是,有多少 MVVM 开发人员对此视而不见。正如您不需要用于控制的视图控制器一样,您也不需要用于虚拟机的视图模型。设计模式是一种@State心灵。不涉及具体的命名和严格的结构。

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

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

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

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

我个人见过 RxSwift 开发人员将控制器代码移至视图,以便控制器显得“干净”,并且需要 3 个第三方库(一个是自定义 fork)来发送 http GET。

如果你不能让简单的事情变得简单,设计模式就没有意义。


Jac*_*sen 1

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

  • 为了管理非示例项目中视图的更新,您几乎总是希望使用 ObservableObject/ObservedObject。
  • 视图应该仅在对象发生变化时需要更新时才观察该对象。如果您可以将更新委托给子视图,那就更好了。
  • @Published创建一个大型 ObservableObject 并添加其所有属性可能很诱人。然而,这意味着观察该对象的视图会更新(有时是可见的),即使视图甚至不依赖的属性发生更改。
  • 绑定是代表可以修改数据的控件的视图最自然的接口。请注意,绑定不会触发更新视图。更新视图应该由@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()
    }
}

Run Code Online (Sandbox Code Playgroud)

这对于您的应用程序来说可能有点过分了。有一个更简单的版本,但它的缺点是文本字段会更新,即使它们的内容没有改变。在这个特定的例子中,我认为这并不重要。对于较大的项目,它可能变得很重要,不仅仅是出于性能原因,但更新有时非常明显。供参考:这是更简单的版本。

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()
    }
}
Run Code Online (Sandbox Code Playgroud)