之前在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