SwiftUI极简教程20:CoreData数据持久化框架的使用(上)

本文涉及的产品
可视分析地图(DataV-Atlas),3 个项目,100M 存储空间
简介: SwiftUI极简教程20:CoreData数据持久化框架的使用(上)

在本章中,你将学会使用CoreData数据持久化框架搭建一个简单的ToDo待办事项App

image.png

我们在之前的学习中构建过List列表和SwipeCard卡片,我们发现如果我们重新启动模拟器,它的数据会恢复原始数组的数据。这是因为每次打开App的时候,系统会根据数据源重新遍历数据,当用户关闭应用程序并重新启动时,所有数据都“消失”了。

那么本章我们学习一个新的框架,叫做CoreData,它一个管理数据对象的框架,可以将我们的数据保存起来,这样每当我们重新打开App的时候,App展示的就是我们上一次操作的数据。


值得注意的一点是,CoreData可不是数据库哦,它只是一个用于开发人员管理和存储数据持久化的交互框架,它的持久存储并不局限于数据库。

好了,说了那么多,让我们正式开始吧。


首先,创建一个新项目,命名为SwiftUICoreData,请注意,这里我们需要勾选使用CoreData

image.png


CoreData框架数据持久化实现原理


我们发现,和以往创建的App不同,这次多了几个文件。

一个是SwiftUICoreData.xcdatamodeld文件,它是管理整个项目生成的对象模型的,是定义实体与持久存储交互的文件。

另一个是Persistence.swift文件,它是数据保存到持久存储区的文件。

image.png

SwiftUI通过将管理对象上下文viewContext注入到环境中,来实现在任何视图都可以检索上下文,并且能够管理数据

我们再看一下SwiftUICoreDataApp.swift文件,可以看到它定义了一个常量persistenceController来保存PersistenceController的实例,并在ContentView主视图中将托管对象上下文viewContext注入到环境中。

image.png

上面我们看到已经在管理对象模型中创建实体了,并且定义一个继承自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框架数据持久化实现的原理,我们可以预览下系统提供的例子。

image.png

下面,让我们进入正题。

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.png

中间的内容部分,我们可以看到有两种情况,一种是没有数据的时候,我们展示一张Image图片,另一种是有数据的时候,展示List数据列表。

NoDataView缺省页


我们导入一张图片,命名叫做image01,然后构建第一种空数据的情况,业务上常常叫做缺省页的图。

//缺省图
struct NoDataView: View {
    var body: some View {
        Image("image01")
            .resizable()
            .scaledToFit()
    }
}


image.png

如果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)
            }
        }
    }
}


image.png

我们在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())


image.png

以上,我们完成了空的列表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()
            }
        }
    }
}


image.png

嗯?为啥List列表会有背景颜色?这是iOS14的新特性,如果我们需要去掉这个颜色,需要再做一下处理,在视图加载的时候,将TableView列表和TableViewCell列表项的背景颜色变成无填充颜.clear


//去掉Listb背景颜色
init() {
    UITableView.appearance().backgroundColor = .clear
    UITableViewCell.appearance().backgroundColor = .clear
}


这样,我们就完成了列表展示页的制作。


image.png

由于章节篇幅太长,将分为上下两章来写,上半部分先完成主要页面的构建,下半部分我们再完成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
        }
    }
}

快来动手试试吧!


相关实践学习
DataV Board用户界面概览
本实验带领用户熟悉DataV Board这款可视化产品的用户界面
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
7月前
|
SQL 存储 开发框架
EntityFramework数据持久化复习资料4、Lambda表达式的使用(重点内容)
EntityFramework数据持久化复习资料4、Lambda表达式的使用(重点内容)
55 0
|
3月前
|
iOS开发 开发者 UED
探索iOS应用开发中的SwiftUI框架
【9月更文挑战第26天】 在iOS开发的海洋中,SwiftUI犹如一艘现代的快艇,引领着开发者们驶向更加高效与直观的编程体验。本文将带你领略SwiftUI的魅力,从其设计理念到实际应用,我们将一步步揭开它如何简化界面构建过程的面纱。通过对比传统方式,你将看到SwiftUI如何让代码变得像诗一样优美,同时保持强大的功能性和灵活性。准备好让你的iOS开发技能加速升级,一起驾驭这股新潮流吧!
|
4月前
|
数据处理 开发者 C#
WPF数据绑定实战:从零开始,带你玩转数据与界面同步,让你的应用程序更上一层楼!
【8月更文挑战第31天】在WPF应用开发中,数据绑定是核心技能之一,它能实现界面元素与数据源的同步更新。本文详细介绍了WPF数据绑定的概念与实现方法,包括属性绑定、元素绑定及路径绑定等技术,并通过示例代码展示了如何创建数据绑定。通过数据绑定,开发者不仅能简化代码、提高可维护性,还能提升用户体验。无论初学者还是有经验的开发者,都能从中受益,更好地掌握WPF数据绑定技巧。
103 0
|
7月前
|
开发框架 前端开发 Swift
SwiftUI的优缺点
SwiftUI的优缺点
230 0
|
7月前
|
编译器 API Swift
【Swift开发专栏】Swift中的SwiftUI框架初探
【4月更文挑战第30天】SwiftUI是苹果2019年推出的界面构建框架,简化iOS应用开发。通过声明式语法和编译器优化,提供直观高效的UI设计。本文将介绍SwiftUI概述、主要特性及实际案例。SwiftUI强调“少即是多”,用少量代码实现复杂界面,提供简洁API、自动布局、双向数据绑定等功能。通过视图组合和实时预览加速开发。案例展示如何用SwiftUI构建用户列表界面,体现其结构清晰、易扩展的优势。SwiftUI在iOS开发中的重要性日益提升。
97 0
|
7月前
|
开发工具 Swift iOS开发
利用SwiftUI构建动态用户界面:iOS开发新范式
【4月更文挑战第3天】 随着苹果不断推进其软件开发工具的边界,SwiftUI作为一种新兴的编程框架,已经逐渐成为iOS开发者的新宠。不同于传统的UIKit,SwiftUI通过声明式语法和强大的功能组合,为创建动态且响应式的用户界面提供了一种更加简洁高效的方式。本文将深入探讨如何利用SwiftUI技术构建具有高度自定义能力和响应性的用户界面,并展示其在现代iOS应用开发中的优势和潜力。
|
7月前
|
SQL 存储 开发框架
EntityFramework数据持久化复习资料6、EntityFramework引入
EntityFramework数据持久化复习资料6、EntityFramework引入
55 0
|
7月前
|
存储 数据库 iOS开发
IOS开发数据存储:什么是 CoreData?如何在应用中使用它?
IOS开发数据存储:什么是 CoreData?如何在应用中使用它?
232 0
|
C# 计算机视觉 开发者
在Winform中一分钟入门使用好看性能还好的Blazor Hybrid
在Winform中一分钟入门使用好看性能还好的Blazor Hybrid
295 0
在Winform中一分钟入门使用好看性能还好的Blazor Hybrid
|
JavaScript 前端开发 中间件
Redux 原理探秘
Redux 是一个非常不错的状态管理库,和 Vuex 不同的是 Redux 并不和 React 强绑定,你甚至可以在 Vue 中使用 Redux。当初的目标是创建一个状态管理库,来提供最简化 API。
141 0