使用类似于MVC的设计模式与SwiftUI

5
我正在尝试实现像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")
        }
        
    
}
}

控制器

var assignmentModel = AssignmentModel()

func retrieveFirstAssignment()
{
    var firstAssignment : Assignment

    firstAssignment=assignmentModel.retrieveFirstAssignment()
    
}

目前它只是找到了对象,但没有进行任何操作。

模型

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

*我们假设任务模型有一个可工作的函数,可以从数据库检索一个任务以便在视图中展示它。


谢谢。这正是我的问题,你应该如何处理与视图的交互,因为视图不应该知道模型的外观。这就是为什么我现在把它留空了。 - ChesterK2
2
当然,你可以使用几乎任何设计模式,但你考虑过MVVM吗?它似乎比MVC更适合SwiftUI的方式,并且苹果在他们的教程系列中采用了它。 - Magnas
我也可以接受MVVM,但在这种情况下,我的问题仍然是如何在视图模型和视图之间建立适当的绑定连接。 - ChesterK2
2
ObservableObject/ObservedObject。 - Magnas
如果有人能提供一个基本的代码示例,使用我使用的演示类(将Controller替换为View Model),那将不胜感激。 - ChesterK2
3个回答

8
struct SwiftUIView: View {

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

这就是我所谓的“内置MVVM增强型MVC”。它在更少的努力下同时满足你对MVC和可能的MVVM的追求。
下面我将解释原因:
1. 函数,或者更具体地说,修改是控制。你不需要一个名为“Controller”的对象来实现MVC中的C。否则我们可以再使用UIKit多10年。
当您的模型是值类型时,这是有道理的。除非您特别允许,否则没有任何东西可以改变它,例如@State注解。由于仅通过这些指定端点修改模型,因此您的控件仅在修改这些端点的函数中生效。
2. 引用另一个答复:
SwiftUI与MVVM比与MVC更加匹配是正确的。然而,在Apple文档中几乎所有示例代码都很简单,ViewModel(和/或MVC中的Controller)完全被省略了。一旦开始创建更大的项目,就会出现需要某种方法来桥接视图和模型的需求。但是,在我看来,SwiftUI文档尚未完全满足这一点。
如果有什么作用,那就是增强MVC。ViewModel的目的是什么?
a. 有Model-View绑定,这在我的代码片段中是存在的。
b. 管理与对象相关联的状态。如果@State没有给您留下它是用于管理状态的印象,那么我不知道还有什么。很有趣,许多MVVM开发人员对此视而不见。正如您不需要View Controller来控制,您也不需要ViewModel来控制VM。设计模式是一种思维方式,而不是特定命名和严格结构。
c. 假设我接受MVVM而不是基于它。哪个代码片段你认为更有可能扩展到更大的项目?我的紧凑版还是建议的另一个答复版本?提示:考虑一下要编写多少额外的文件,视图模型,observableobjects,胶水代码,传递vm作为参数,接受vm作为参数的init函数等。这些只是为了在另一个对象中编写一些代码。这并没有减少或简化手头的任务。它甚至没有告诉您如何重构控件代码,因此您很可能会再次重复MVC中错误的事情。我是否提到ViewModel是带有隐含状态管理的共享引用类型对象?那么如果您只是使用引用类型模型覆盖它,那还有什么意义呢?
有趣的是,MVVM开发人员说SwiftUI在其基本形式下无法扩展到更大的项目。保持简单是唯一的扩展方式。
这是我在2020年观察到的开发进展路线图。
Day1: 初学者 Day2: 搜Google一些,丢弃MVC Day3: 继续搜Google,SwiftUI 不可扩展 Day4: 好了,我需要 MVVM+RxSwift+Coordinator+Router+DependencyInjection 来避免 SDK 的缺陷。
我的建议是,因为这似乎是常见的初学者问题,所以要先学会走再学会跑。
我个人曾见过 RxSwift 开发者将控制器代码移动到视图中,以使控制器看起来“干净”,并需要三个第三方库(其中一个是自定义分支)来发送 Http Get 请求。
如果你不能简单地完成简单的事情,那么设计模式就没有意义。

1

我曾经有同样的问题,后来发现了Matteo Manferdini的一篇优秀论文,其中描述了如何在SwiftUI中使用MVC模型。 我使用他的论文重构了一个使用CoreData的Pizza应用程序。尽管我在SwiftUI方面仍然是初学者,但这让我非常清楚如何实现MVC。你可以在这里找到Matteo的论文。


1
对我来说,这是一个非常好的问题。 SwiftUI 确实比 MVC 更接近 MVVM。然而,几乎所有在 Apple 文档中的示例代码都非常简单,ViewModel(和/或 MVC 中的 Controller)完全被省略了。一旦你开始创建更大的项目,就需要某种东西来连接你的视图和模型。然而,在我看来,SwiftUI 文档还没有(完全)以令人满意的方式解决这个问题。我希望其他开发者能够纠正我或者扩展我的观点(我还在学习),但是这是我目前所发现的。
  • 管理非示例项目中视图更新,你几乎总是希望使用 ObservableObject/ObservedObject。
  • 仅当视图需要在更改时更新才应观察对象。最好的做法是将更新委托给子视图。
  • 可能会想要创建一个大的 ObservableObject,并为其所有属性添加 @Published。然而,这意味着观察该对象的视图会得到更新(有时会可见),即使视图并不依赖于某个属性的更改。
  • Binding 是表示可以修改数据的控件的视图的最自然接口。请注意,Binding 不会触发更新视图。更新视图应由 @State 或 @ObservedObject 管理(可以由控件的父视图完成)。
  • Constants 是仅显示数据而不修改数据的视图的自然接口。

以下是如何将此应用于您的示例:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接