一同养成写作习惯!这是我参与「日新方案 4 月更文应战」的第20天,点击查看活动概况。
今天职言:深化思考和坚持平等重要。
在本章中,你将学会运用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
}
}
}
快来着手试试吧!
假如本专栏对你有协助,无妨点赞、评论、关注~