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 Plugin 从零开始到上架 - iOS总结

Day6 输入框介绍

看完上一篇的输入,今天来介绍不一样表单元件 Input text Textarea多行文字 Chec...

【从实作学习ASP.NET Core】Day05 | Model 模型

今天我们要让程序加上 Model 来串接资料库,让 Controller 向 Model 取得商品...

【Day 17】Django model

续昨天 我们使用 Python 预设的资料库模组-SQLite,昨天讲到的 Database 就会长...

Day 19 - 在SQL server 建立Table以及设计资料表

昨天我们讲到如何在SQL server建立DB,但像是Table、Column之类的该如何建置呢,很...

电子书阅读器上的浏览器 [Day04] 以翻页的方式浏览网页

在 FOSS Browser (以後简称 browser) 中已经有支援简单的手势操作,让使用者可以...