承接上一章的内容,我们继续完成使用CoreData
框架搭建一个简单的ToDo
待办事项App
。
这一章节,我们正式进入使用CoreData
框架实现数据持久化
。
之前我们是通过创建项目的时候勾选使用CoreData
框架,系统给我们创建了需要的文件
和模型
。那么这一章节,我们尝试使用CoreData
框架达到数据持久化
的目的。
Persistence.swift文件
Persistence.swift
文件是数据保存
到持久存储区的文件,通过将管理对象上下文注入
到环境中,来实现在任何视图都可以检索
上下文,并且能够管理数据
。
我们删除
多余的示例数据代码。
import Foundation import CoreData struct PersistenceController { static let shared = PersistenceController() let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "SwiftUICoreData") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } container.loadPersistentStores(completionHandler: { storeDescription, error in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) } }
SwiftUICoreDataApp.swift文件
我们回到SwiftUICoreDataApp.swift
文件,我们需要将托管对象上下文注入到环境中,这样我们就能够方便地在内容视图中访问上下文,并且管理数据库中的数据。
import SwiftUI @main struct SwiftUICoreDataApp: App { let persistenceController = PersistenceController.shared var body: some Scene { WindowGroup { ContentView() .environment(\.managedObjectContext, persistenceController.container.viewContext) } } }
SwiftUICoreData.xcdatamodeld模型
模型创建完成后,我们需要创建一个新的实体ToDoItem
来存储我们需要用到的参数
,我们将原来的实体Item
重新命名为ToDoItem
。
在ToDoItem
实体中,我们需要定义好项目需要的属性
。
id:UUID name:String priorityNum:Integer32 isCompleted:Boolean
由于priority
优先级属性是一个Enum
枚举类型,为了将Enum
枚举类型保存
到数据库中,我们必须存储它的原始值
,即Int
整数,因此需要使用Integer 32
类型,而且为了避免命名冲突
,我们将属性命名为priorityNum
。
这里再科普一个知识点。
由于ToDoItem
实体我们重新定义了,那么要保证Module
模块要选择CurrrentProductModule
当前产品的模型,Codegen
代码基因要选择Manual/None
,不然我们在项目中引用模型的时候可能会找不到
我们定义的ToDoItem
实体,这非常重要!非常重要!非常重要!重要的事情说三遍。
CoreData模型类
在CoreData
框架中,每个实体都与一个模型类配对
,我们需要在ToDoItem.swift
文件定义模型类。
这里CoreData
的模型类继承自NSManagedObject
协议,每个属性都使用@NSManaged
进行注释,并且对应我们ToDoItem
创建的属性id、name、priorityNum、isCompleted
。
import Foundation import CoreData enum Priority: Int { case low = 0 case normal = 1 case high = 2 } public class ToDoItem: NSManagedObject { @NSManaged public var id: UUID @NSManaged public var name: String @NSManaged public var priorityNum: Int32 @NSManaged public var isCompleted: Bool } extension ToDoItem: Identifiable { var priority: Priority { get { return Priority(rawValue: Int(priorityNum)) ?? .normal } set { self.priorityNum = Int32(newValue.rawValue) } } }
ContentView.swift文件
定义好CoreData
模型类后,我们需要在ContentView
主视图中使用它,我们使用@FetchRequest
属性包装器从数据库加载数据
,我们替换之前使用@State
标记的ToDoItem
数组数据来源。
@FetchRequest( entity: ToDoItem.entity(), sortDescriptors: [ NSSortDescriptor(keyPath: \ToDoItem.priorityNum, ascending: false) ]) var todoItems: FetchedResults<ToDoItem> 复制代码
我们在环境中注入了托管对象上下文,获取所需的数据。
由于我们的List
列表数据来源于新定义的todoItems
,我们把整个ToDoListView
的body
内容拆回到ContentView
主视图,这样我们就可以直接
遍历循环todoItems
的内容。
List { ForEach(todoItems) { todoItem in ToDoListRow(todoItem: todoItem) }
NewToDoView.swift文件
紧接着,我们需要同步更新NewToDoView.swift
的代码。要将一个新任务保存到数据库中,首先需要从环境中获取管理对象上下文。
@Environment(\.managedObjectContext) var context
然后,我们在NewToDoView
视图和saveButton
保存按钮视图就不需要再@Binding
绑定ToDoItem
数组了。
@Binding var todoItems: [ToDoItem]
然后,我们再更新下addTask
添加新事项的方法。
//添加新事项方法 private func addTask(name: String, priority: Priority, isCompleted: Bool = false) { let task = ToDoItem(context: context) task.id = UUID() task.name = name task.priority = priority task.isCompleted = isCompleted do { try context.save() } catch { print(error) } }
我们要将新事项
插入数据库中,需要使用托管上下文创建ToDoItem
,然后调用上下文的save
函数来提交更改。
因为我们删除了todoItems
绑定,所以还需要调整NewToDoView_Previews
预览视图。
NewToDoView(name: "", priority: .normal, showNewTask: .constant(true))
由于我们NewToDoView
新建事项视图发现变化
,我们回到ContentView
首页视图,重新修改下绑定
关系。
NewToDoView(name: "", priority: .normal, showNewTask: $showNewTask)
同样,我们在ContentView
首页视图也需要从环境中获取管理对象上下文。
@Environment(\.managedObjectContext) var context
ToDoListRow视图
与添加新事项类似,我们需要获取用于记录更新的托管对象上下文。对于Toggle
优先级选择视图,每当切换发生更改时,todoItem
的isCompleted
属性将被更新。我们可以附加onReceive
修饰符,onReceive
修饰符可以监听isCompleted
属性和其他我们定义好的属性的更改,并通过调用上下文的save
函数将它们保存到持久存储区。
//监听todoItem数组参数变化并保存 .onReceive(todoItem.objectWillChange, perform: { _ in if self.context.hasChanges { try? self.context.save() } })
deleteTask删除事项方法
上面我们既然完成了addTask
新增事项方法,那么顺便完成deleteTask
删除事项的交互。
//删除事项方法 private func deleteTask(indexSet: IndexSet) { for index in indexSet { let itemToDelete = todoItems[index] context.delete(itemToDelete) } DispatchQueue.main.async { do { try context.save() } catch { print(error) } } }
deleteTask
删除事项方法接收一个存储要删除的项
的索引集,我们只需要调用上下文的delete
函数,并指定要删除的项,然后调用save
函数来提交更新。
我们把这个方法加到List
里,就可以实现滑动删除
的操作。
List { ForEach(todoItems) { todoItem in ToDoListRow(todoItem: todoItem) }.onDelete(perform: deleteTask) }
preview预览效果
我们运行模拟器的时候发现报错了,这是因为我们没有在contentview_preview
结构体中注入托管对象上下文。我们需要创建一个内存中的数据存储,并准备一些测试数据。
我们在Persistence.swift
文件创建了一个PersistenceController
实例,并将inMemory
参数设置为true
,然后我们添加10
个示例待办事项,并将它们保存
到数据存储中。
import CoreData struct PersistenceController { static let shared = PersistenceController() let container: NSPersistentContainer static var preview: PersistenceController = { let result = PersistenceController(inMemory: true) let viewContext = result.container.viewContext for index in 0..<10 { let newItem = ToDoItem(context: viewContext) newItem.id = UUID() newItem.name = "待办事项\(index)" newItem.priority = .normal newItem.isCompleted = false } do { try viewContext.save() } catch { let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } return result }() init(inMemory: Bool = false) { container = NSPersistentContainer(name: "SwiftUICoreData") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) } }
之后我们如果只需要在ContentView_Previews
预览视图的上下文注入到内容视图的环境中,就可以看到示例
的数据啦~
struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) } }
快来动手试试吧!