iT邦幫忙

2021 iThome 鐵人賽

0
Software Development

iOS 學習筆記系列 第 29

Day29 Data Storage in iOS 05 - Core Data 實作專案範例

之前在Android 就接觸過MVC、MVP以及MVVM,這邊先不對各差別去作比較分析,直接來對MVVM 做個簡單的介紹

Model-View-ViewModel (MVVM)

MVVM 比 MVC 模型多添加了一個組件,稱為 ViewModel(視圖模型),它可以是 class 或 struct,但通常會是一個 class,因此可以在程式碼中傳遞同一個物件的 reference,而 ViewModel 就位於視圖控制器和模型之間,其中這個模式的核心是ViewModel,它是一種特殊的model型別,用於表示程式的UI狀態。它包含描述每個UI控制元件的狀態的屬性。

SwiftUI 內建 MVVM

在最簡單的情況下,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 專案

  1. 首先在Xcode 建立一個App 專案CoreDataSample,接下來新增Data Model,所建立的ManagedObjectModel會實例化PersistentStoreCoordinator,讓PersistentStoreCoordinator知道應用程序的類型、屬性和關係

    選擇建立Data Model:

    輸入你的名稱後按create 按鈕建立

    產生Persons.xcdatamodeld後,選取它後建立Core Data Model 的實體(Entity)

    並設定Entity 名稱與其屬性與關聯性

  2. 接下來我們建立一個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)")
                }
            }
        }
    }
    
    
  3. 接下來建立一個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)
        }
    }
    
    
  4. 最後在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 Data Storage in iOS 04 - Core Data 簡介
下一篇
Day30 完賽心得
系列文
iOS 學習筆記30

尚未有邦友留言

立即登入留言