在本章中,你将学会使用CoreData
数据持久化框架搭建一个简单的ToDo
待办事项App
。
我们在之前的学习中构建过List
列表和SwipeCard
卡片,我们发现如果我们重新启动模拟器,它的数据会恢复原始数组的数据。这是因为每次打开App
的时候,系统会根据数据源重新遍历数据,当用户关闭应用程序并重新启动时,所有数据都“消失
”了。
那么本章我们学习一个新的框架,叫做CoreData
,它一个管理数据对象的框架,可以将我们的数据保存起来,这样每当我们重新打开App
的时候,App
展示的就是我们上一次操作的数据。
值得注意的一点是,CoreData
可不是数据库哦,它只是一个用于开发人员管理和存储数据持久化
的交互框架,它的持久存储并不局限于
数据库。
好了,说了那么多,让我们正式开始吧。
首先,创建一个新项目,命名为SwiftUICoreData
,请注意,这里我们需要勾选使用CoreData
。
CoreData框架数据持久化实现原理
我们发现,和以往创建的App
不同,这次多了几个文件。
一个是SwiftUICoreData.xcdatamodeld
文件,它是管理整个项目生成的对象模型的,是定义实体与持久存储交互
的文件。
另一个是Persistence.swift
文件,它是数据保存到持久存储区
的文件。
SwiftUI
通过将管理对象上下文viewContext
注入到环境中,来实现在任何视图
都可以检索
上下文,并且能够管理数据
。
我们再看一下SwiftUICoreDataApp.swift
文件,可以看到它定义了一个常量persistenceController
来保存PersistenceController
的实例,并在ContentView
主视图中将托管对象上下文viewContext
注入到环境中。
上面我们看到已经在管理对象模型中创建实体了,并且定义一个继承自NSManagedObject
的管理对象来与实体关联。
我们回到ContentView.swift
文件,可以看到系统生成了一堆的示例代码
,让我们解读一下。
首先使用了@Environment
环境变量从环境中获取托管对象上下文viewContext
:
@Environment(\.managedObjectContext) var context
然后创建管理对象,并使用context
上下文的save
方法将对象添加到数据库
中:
//示例代码 let task = ToDoItem(context: context) task.id = UUID() task.name = name task.priority = priority task.isCompleted = isCompleted
在数据检索
方面,我们引入了一个名为@FetchRequest
的属性包装器,用于从持久存储中获取数据。它可以指定要检索的实体对象以及数据的排序方式,然后,CoreData
框架就可以将使用@Environment
环境的托管对象上下文context
来获取数据。
@FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],animation: .default) private var items: FetchedResults<Item>
好了,以上就是CoreData
框架数据持久化实现的原理,我们可以预览下系统提供的例子。
下面,让我们进入正题。
ToDoItem类准备
首先,我们需要定义一个模型类,我们可以创建一个新的文件,点击Xcode
顶部导航栏,File
文件,New
新建,选择File
创建文件,选择iOS
中的Swift File
类型的文件,命名为ToDoItem.swift
。
然后我们构建需要App
需要的参数。
我们先构建一个枚举类型Priority
,来表示我们任务的优先级,分别是低、中、高、最高,用数值Int
类型表示权重。
//任务紧急程度的枚举 enum Priority: Int { case low = 0 case normal = 1 case high = 2 }
然后定义一个类ToDoItem
遵循ObservableObject
可被观察对象协议和Identifiable
可被识别协议,在ToDoItem
类里面有三个参数:name
名称、priority
优先级、isCompleted
是否完成。并且在ObservableObject
协议需要使用@Published
定义,这样才能在参数改变的时候检测到变化
。
至于遵循Identifiable
协议就不用说了,我们定义id
作为每一个任务项的唯一标识符
,这样即便是相同名称、相同优先级的任务,系统也不会把它们作为同一个,这个我们之前的章节讲过。
//ToDoItem遵循ObservableObject协议 class ToDoItem: ObservableObject, Identifiable { var id = UUID() @Published var name: String = "" @Published var priority: Priority = .high @Published var isCompleted: Bool = false //实例化 init(name: String, priority: Priority = .normal, isCompleted: Bool = false) { self.name = name self.priority = priority self.isCompleted = isCompleted } }
我们回到ContentView.swift
文件,我们看看需要做哪些东西。
TopBarMenu顶部导航栏
首先是TopBarMenu
顶部导航栏,比较简单,在这里就不赘述了。
//顶部导航栏 struct TopBarMenu: View { var body: some View { HStack { Text("待办事项") .font(.system(size: 40, weight: .black)) Spacer() Button(action: { }) { Image(systemName: "plus.circle.fill") .font(.largeTitle).foregroundColor(.blue) } } .padding() } }
中间的内容部分,我们可以看到有两种情况,一种是没有数据的时候,我们展示一张Image
图片,另一种是有数据的时候,展示List
数据列表。
NoDataView缺省页
我们导入一张图片,命名叫做image01
,然后构建第一种空数据的情况,业务上常常叫做缺省页的图。
//缺省图 struct NoDataView: View { var body: some View { Image("image01") .resizable() .scaledToFit() } }
如果List
列表有数据的时候,我们需要展示列表数据,接下来,我们完成下List
的创建。
ToDoListView列表页创建
之前的章节我们了解过List
列表的创建方式,这里我们先构建单个任务项ToDoListRow
视图的样式,然后使用List
列表+ForrEach
循环的方法构建整个列表ToDoListView
。
// 列表 struct ToDoListView: View { @Binding var todoItems: [ToDoItem] var body: some View { List { ForEach(todoItems) { todoItem in ToDoListRow(todoItem: todoItem) } } } } // 列表内容 struct ToDoListRow: View { @ObservedObject var todoItem: ToDoItem var body: some View { Toggle(isOn: self.$todoItem.isCompleted) { HStack { Text(self.todoItem.name) .strikethrough(self.todoItem.isCompleted, color: .black) .bold() .animation(.default) Spacer() Circle() .frame(width: 20, height: 20) } } } }
我们在ToDoListView
列表视图使用@Binding
(图中有误)声明了一个todoItems
状态,用来存储ToDoItem
数组,当数据变化时就刷新页面。
//ContentView视图 VStack { TopBarMenu() ToDoListView(todoItems: $todoItems) }
然后我们在ToDoListRow
视图使用@ObservableObject
声明了一个todoItem
,用来引用定义好的实例化方法。
对于ToDoListRow
单个任务项的视图,里面也比较简答,我们用了一个Toggle
开关作为复选框,再加上一个Text
文字作为待办事项的内容标题,最后我们还用了一个Circle
圆形的形状,作为priority
标识。
priority
标识我们可以定义一个私有的颜色方法,当我们从Priority
枚举类型中获得不同状态时,返回不同的颜色,比如优先级高
显示红色
,一般优先级
显示橘色
,低优先级
显示绿色
。
// 根据优先级显示不同颜色 private func color(for priority: Priority) -> Color { switch priority { case .high: return .red case .normal: return .orange case .low: return .green } }
定义好方法后,我们将Circle
圆形赋予背景颜色,颜色值调用priority
定义颜色方法。
.foregroundColor(self.color(for: self.todoItem.priority))
然后对于Toggle
开关,我们希望用的是checkbox
复选框的样式,还记得之前的章节中我们用ButtonStyle
修改Button
按钮的样式么?
是的,Toggle
开关也支持自定义样式的方式,我们可以用ToggleStyle
开关样式把Toggle
开关变成checkbox
复选框。
// checkbox复选框样式 struct CheckboxStyle: ToggleStyle { func makeBody(configuration: Self.Configuration) -> some View { return HStack { Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle") .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .purple : .gray) .font(.system(size: 20, weight: .bold, design: .default)) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
然后,我们给Toggle
开关添加.toggleStyle
开关样式修饰符就可以将自定义好的样式加到里面了。
.toggleStyle(CheckboxStyle())
以上,我们完成了空的列表NoDataView
缺省页,还有有数据时的列表ToDoListView
待办事项列表,当然现在ToDoListView
待办事项列表还没有数据,别急,我们慢慢来。
页面展示逻辑判断
那么什么时候展示NoDataView
缺省页视图,什么时候展示ToDoListView
待办事项列表视图呢?
当然是todoItems
没有数据的时候展示NoDataView
缺省页视图,todoItems
有数据的时候展示ToDoListView
待办事项列表视图。
我们就可以把这个判断加到ContentView
主视图里面。
if todoItems.count == 0 { NoDataView() }
最后,在ContentView
主视图布局部分,我们将TopBarMenu
顶部导航栏、ToDoListView
待办事项列表用VStack
垂直排布在一起,然后使用ZStack
层叠视图将NoDataView
缺省页视图包裹在一起看看效果。
//主视图 struct ContentView: View { @State var todoItems: [ToDoItem] = [] var body: some View { ZStack { VStack { TopBarMenu() ToDoListView() } if todoItems.count == 0 { NoDataView() } } } }
嗯?为啥List
列表会有背景颜色?这是iOS14
的新特性,如果我们需要去掉这个颜色,需要再做一下处理,在视图加载的时候,将TableView
列表和TableViewCell
列表项的背景颜色变成无填充颜.clear
。
//去掉Listb背景颜色 init() { UITableView.appearance().backgroundColor = .clear UITableViewCell.appearance().backgroundColor = .clear }
这样,我们就完成了列表展示页的制作。
由于章节篇幅太长,将分为上下两章来写,上半部分先完成主要页面的构建,下半部分我们再完成NewToDoView
新增任务项页面和基于CoreData
框架数据持久化的逻辑部分。
本章完整代码如下:
//ToDoItem.swift import Foundation enum Priority: Int { case low = 0 case normal = 1 case high = 2 } class ToDoItem: ObservableObject, Identifiable { var id = UUID() @Published var name: String = "" @Published var priority: Priority = .high @Published var isCompleted: Bool = false init(name: String, priority: Priority = .normal, isCompleted: Bool = false) { self.name = name self.priority = priority self.isCompleted = isCompleted } }
//ContentView.swift import CoreData import SwiftUI struct ContentView: View { @State var todoItems: [ToDoItem] = [] //去掉Listb背景颜色 init() { UITableView.appearance().backgroundColor = .clear UITableViewCell.appearance().backgroundColor = .clear } var body: some View { ZStack { VStack { TopBarMenu() ToDoListView(todoItems: $todoItems) } if todoItems.count == 0 { NoDataView() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } // 顶部导航栏 struct TopBarMenu: View { var body: some View { HStack { Text("待办事项") .font(.system(size: 40, weight: .black)) Spacer() Button(action: { }) { Image(systemName: "plus.circle.fill") .font(.largeTitle).foregroundColor(.blue) } } .padding() } } // 缺省图 struct NoDataView: View { var body: some View { Image("image01") .resizable() .scaledToFit() } } // 列表 struct ToDoListView: View { @Binding **var** showNewTask: Bool var body: some View { List { ForEach(todoItems) { todoItem in ToDoListRow(todoItem: todoItem) } } } } // 列表内容 struct ToDoListRow: View { @ObservedObject var todoItem: ToDoItem var body: some View { Toggle(isOn: self.$todoItem.isCompleted) { HStack { Text(self.todoItem.name) .strikethrough(self.todoItem.isCompleted, color: .black) .bold() .animation(.default) Spacer() Circle() .frame(width: 20, height: 20) .foregroundColor(self.color(for: self.todoItem.priority)) } }.toggleStyle(CheckboxStyle()) } // 根据优先级显示不同颜色 private func color(for priority: Priority) -> Color { switch priority { case .high: return .red case .normal: return .orange case .low: return .green } } } // checkbox复选框样式 struct CheckboxStyle: ToggleStyle { func makeBody(configuration: Self.Configuration) -> some View { return HStack { Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle") .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .purple : .gray) .font(.system(size: 20, weight: .bold, design: .default)) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
快来动手试试吧!