之前在Android 就接触过MVC、MVP以及MVVM,这边先不对各差别去作比较分析,直接来对MVVM 做个简单的介绍
MVVM 比 MVC 模型多添加了一个组件,称为 ViewModel(视图模型),它可以是 class 或 struct,但通常会是一个 class,因此可以在程序码中传递同一个物件的 reference,而 ViewModel 就位於视图控制器和模型之间,其中这个模式的核心是ViewModel,它是一种特殊的model型别,用於表示程序的UI状态。它包含描述每个UI控制元件的状态的属性。
在最简单的情况下,View 不依赖於任何外部状态,它的本地 @State 变量扮演 ViewModel 的角色,提供订阅机制(绑定)以在状态更改时刷新 UI。
对於更复杂的场景,视图可以引用外部 ObservableObject,在这种情况下可以是不同的 ViewModel。
无论如何,SwiftUI 视图处理状态的方式非常类似於经典的 MVVM(除非我们引入更复杂的编程实体图)。
还有很多关於 Clean Architecture and MVVM on iOS 的内容,但这边就不做详细介绍
推荐文章:
Clean Architecture for SwiftUI
Clean Architecture and MVVM on iOS
这边做一个简单的MVVM 架构的Core Data 专案
首先在Xcode 建立一个App 专案CoreDataSample
,接下来新增Data Model,所建立的ManagedObjectModel
会实例化PersistentStoreCoordinator
,让PersistentStoreCoordinator
知道应用程序的类型、属性和关系
选择建立Data Model:
输入你的名称後按create 按钮建立
产生Persons.xcdatamodeld
後,选取它後建立Core Data Model 的实体(Entity)
并设定Entity 名称与其属性与关联性
接下来我们建立一个CoreDataManager.swift
来准备建立NSPersistentContainer
的实例,之後会透过单例去管理对Core Data 的任何存取等动作,首先初始化persistentContainer
需要透过loadPersistentStores(NSPersistentStoreDescription)
来指定container
载入前面建立的Persistent Stores 与建立Core Data stack,如果载入资料时发生错误则会生出一个NSError
的值
import Foundation
import CoreData
class CoreDataManager {
let persistentContainer: NSPersistentContainer
static let shared = CoreDataManager()
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
private init() {
persistentContainer = NSPersistentContainer(name: "Persons")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Unable to initialize Core Data Stack \(error)")
}
}
}
}
接下来建立一个PersonListViewModel
来让View 层透过此ViewModel
来取得所需要的资料,为此我们需要先在CoreDataManager
新增对资料库存取的方法以供PersonListViewModel
调用
class CoreDataManager {
//Net Added
func getPersonById(id: NSManagedObjectID) -> PersonItem? {
do {
return try viewContext.existingObject(with: id) as? PersonItem
} catch {
return nil
}
}
func deletePerson(person: PersonItem) {
viewContext.delete(person)
save()
}
func getAllPersons() -> [PersonItem] {
let request: NSFetchRequest<PersonItem> = PersonItem.fetchRequest()
do {
return try viewContext.fetch(request)
} catch {
return []
}
}
func save() {
do {
try viewContext.save()
} catch {
viewContext.rollback()
print(error.localizedDescription)
}
}
// ...
然後建立PersonListViewModel
import Foundation
import CoreData
class PersonListViewModel: ObservableObject {
var name: String = ""
var height: String = ""
var weight: String = ""
@Published var persons: [PersonViewModel] = []
func getAllPersons() {
persons = CoreDataManager.shared.getAllPersons().map(PersonViewModel.init)
}
func delete(_ person: PersonViewModel) {
let existingPerson = CoreDataManager.shared.getPersonById(id: person.id)
if let existingPerson = existingPerson {
CoreDataManager.shared.deletePerson(person: existingPerson)
}
}
func save() {
let person = PersonItem(context: CoreDataManager.shared.viewContext)
person.name = name
person.height = (height as NSString).floatValue
person.weight = (weight as NSString).floatValue
CoreDataManager.shared.save()
}
}
struct PersonViewModel {
let personItem: PersonItem
var id: NSManagedObjectID {
return personItem.objectID
}
var name: String {
return personItem.name ?? ""
}
var height: Float {
return personItem.height
}
var weight: Float {
return personItem.weight
}
}
extension Float {
var clean: String {
return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self)
}
}
最後在View 端调用ViewModel
取得资料,并显示画面就完成了
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = PersonListViewModel()
var body: some View {
VStack {
TextField("Enter person name", text: $viewModel.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Enter person height", text: $viewModel.height)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
TextField("Enter person weight", text: $viewModel.weight)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
Button("Save") {
viewModel.save()
viewModel.getAllPersons()
}
List {
ForEach(viewModel.persons, id: \.id) { person in
Text("name: \(person.name), h: \(person.height.clean), w: \(person.weight.clean)")
}.onDelete(perform: deletePerson)
}
Spacer()
}.padding()
.onAppear(perform: {
viewModel.getAllPersons()
})
}
func deletePerson(at offsets: IndexSet) {
offsets.forEach { index in
let task = viewModel.persons[index]
viewModel.delete(task)
}
viewModel.getAllPersons()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
这边也附上程序码 Github: CoreDataSample
<<: 简报版-第十八章-从假冒电商来电诈骗事件多想想相关风险
>>: Day28 Plugin 从零开始到上架 - iOS总结
看完上一篇的输入,今天来介绍不一样表单元件 Input text Textarea多行文字 Chec...
今天我们要让程序加上 Model 来串接资料库,让 Controller 向 Model 取得商品...
续昨天 我们使用 Python 预设的资料库模组-SQLite,昨天讲到的 Database 就会长...
昨天我们讲到如何在SQL server建立DB,但像是Table、Column之类的该如何建置呢,很...
在 FOSS Browser (以後简称 browser) 中已经有支援简单的手势操作,让使用者可以...