持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
coredata 是用于持久化存储数据的,可以把它的作用简单理解为类似于前端浏览器的 localStorage。但是当你把 APP 删除的时候,APP 对应的 coredata 数据也会被删除。
本文旨在快速讲清 coredata 的开发使用。
通常有借助 List 视图来讲解 coredata 的使用,这是一种常用的方式。但除此之外,也应当有更加通用的方式来使用 coredata,也就是下文将会讲解的增删改查内容。
学会了以下的内容,即使脱离了 List ,你也能单独实现其中的某一项功能:
- 如何安装 coredata
- 创建 coredata 实体和属性
- coredata 如何增加数据
- coredata 如何删除数据
- coredata 如何查询数据
- coredata 如何修改数据
话不多说,让我们开始吧。
下文将会使用 Xcode 14、SwiftUI 开发。
安装 coredata
新建的项目安装 coredata
- 创建一个新项目
- 选择 ios app
- 勾选 use Core Data
打开项目的 HelloCoreData.xcdatamodeld 文件,可以看到已经默认创建了一个名为 Item 的实体。
到这里,一个新项目安装 coredata 的部分就完成了。
现有项目安装 coredata
- 新建一个 Data Model 文件。
- 文件名一般和项目名称一样。
- 创建 Persistence.swift 文件
创建 Persistence,是为了让预览也能使用 coredata 数据;以下是一个官方模板,直接使用即可。
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
// 给预览添加预设数据
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "HelloCoreData")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
创建 coredata 实体和属性
在上文安装和配置好 coredata 后,接下来就可以创建实体和相应的属性了。
创建实体
在这里我们创建一个名为 User 的实体。
实体的名称一般采用首字符大写驼峰的命名方式。
添加了 3 个属性。
创建实体模型
创建完实体后,我们还需要创建一个实体对应的模型,Xcode 也提供了自动生成实体模型的功能,但这里我们采用手动创建实体的方式。
- 首先设置 User 实体的 Class 属性。
- 然后创建一个模型:Models/User.swift
import Foundation
import CoreData
final class User: NSManagedObject {
@NSManaged var id: UUID
// 用户名
@NSManaged var name: String
// 爱好
@NSManaged var hobby: String
}
创建视图
接下来开始完成UI页面的编写。
创建一个名为 UserList.swift 文件。
我们会从 ContentView.swift 使用 NavigationLink 导航到 UserList.swift 页面。
先给预览配置 .environment 修饰符,这样预览后续才能正确显示 coredata 数据。
再新增一个 @Environment 属性,下面的增删改查功能都会用到 viewContext。
只要你在视图中操作 coredata,基本都需要设置 @Environment 属性。
import SwiftUI
struct UserList: View {
@Environment(\.managedObjectContext) private var viewContext
var body: some View {
Text("Hello, World!")
}
}
struct UserList_Previews: PreviewProvider {
static var previews: some View {
UserList()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
到这里,基本的准备工作和 UI 我们都已完成,接下就是实际操作了。
此时预览还不能正常显示,下面我们会修复这个问题。
coredata 查询数据
为了在 UserList 视图中显示用户列表数据,我们需要使用 @FetchRequest 来获取数据:
import SwiftUI
struct UserList: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: User.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],
animation: .default)
// 这就是我们获取到 coredata user 的数据
private var userList: FetchedResults<User>
var body: some View {
Text("Hello, World!")
}
}
struct UserList_Previews: PreviewProvider {
static var previews: some View {
UserList()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
接着使用 ForEach 来显示数据:
struct UserList: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: User.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],
animation: .default)
private var userList: FetchedResults<User>
var body: some View {
VStack {
if userList.isEmpty {
Text("暂无用户数据")
} else {
ForEach(userList, id: \.self) { item in
HStack {
Text(item.name)
Text("爱好:\(item.hobby)")
}
}
}
}.navigationTitle("用户列表")
}
}
但现在预览还无法正常显示,我们需要在 Persistence.swift 文件中给预览添加一些数据用于显示:
for index in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
// 添加的预览数据
let userItem = User(context: viewContext)
userItem.id = UUID()
userItem.name = "用户 \(index)"
userItem.hobby = "篮球"
}
查询数据部分就完成了,是不是很简单。
不管你在哪一个视图中使用了 coredata 数据,要想让该视图正常预览,都需要在 Persistence.swift 添加相应的预览数据。当然,就算不添加预览数据,也不影响模拟器启动。
coredata 新增数据
在新增数据前,我们先简单添加一些UI控件。
下面的代码中将会省略预览的 UserList_Previews 代码。
自定义导航栏,添加一个返回按钮和新增按钮:
import SwiftUI
struct UserList: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: User.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],
animation: .default)
private var userList: FetchedResults<User>
@State private var showAdd = false
var body: some View {
VStack {
if userList.isEmpty {
Text("暂无用户数据")
} else {
ForEach(userList, id: \.self) { item in
HStack {
Text(item.name)
Text("爱好:\(item.hobby)")
}
}
}
}
.padding()
.navigationTitle("用户列表")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
}){
//按钮及其样式
Image(systemName: "chevron.left")
}, trailing: Button(action : {
self.showAdd = true
}){
Image(systemName: "plus")
})
}
}
创建表单和数据字段:
import SwiftUI
struct UserList: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: User.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \User.id, ascending: false)],
animation: .default)
private var userList: FetchedResults<User>
@State private var showAdd = false
// 表单绑定数据
@State private var name: String = ""
@State private var hobby: String = ""
func addUser() {
}
var body: some View {
VStack {
if showAdd {
VStack {
TextField("用户名", text: $name)
.padding()
.border(Color.gray)
TextField("爱好", text: $hobby)
.padding()
.border(Color.gray)
Button(action: {
addUser()
}, label: {
Text("保存")
.padding(.horizontal, 2)
.padding(.vertical, 15)
.frame(maxWidth: .infinity)
.background(Color.blue)
.foregroundColor(.white).cornerRadius(24)
})
}.padding()
}
if userList.isEmpty {
Text("暂无用户数据")
} else {
ForEach(userList, id: \.self) { item in
HStack {
Text(item.name)
Text("爱好:\(item.hobby)")
}
}
}
}
.padding()
.navigationTitle("用户列表")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
}){
//按钮及其样式
Image(systemName: "chevron.left")
}, trailing: Button(action : {
self.showAdd = true
}){
Image(systemName: "plus")
})
}
}
UI准备工作完成,现在新增数据时,我们需要通过 viewContext 获取到实体,然后给实体的属性赋值。
func addUser() {
withAnimation {
if !name.isEmpty && !hobby.isEmpty {
let newItem = User(context: viewContext)
newItem.id = UUID()
newItem.name = name
newItem.hobby = hobby
// ...
}
}
}
最后,保存上下文。
调用 save() 保存数据很重要,如果不保存,即使数据新增成功,数据是没有真正存储到内存中的。
func addUser() {
withAnimation {
if !name.isEmpty && !hobby.isEmpty {
let newItem = User(context: viewContext)
newItem.id = UUID()
newItem.name = name
newItem.hobby = hobby
do {
try viewContext.save()
showAdd = false
} catch {
let nsError = error as NSError
// fatalError() 使应用程序生成崩溃日志并终止。 尽管此功能在开发过程中可能很有用,不应在生产应用程序中使用此功能。
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
演示效果:
到这里,新增数据部分完成。
tips: 在模拟器输入中文:在设置-通用-语言与地区中添加简体中文的语言,在设置-通用-键盘添加简体中文输入法后,在模拟器中按 Command + K,调起软键盘,点击软键盘下面那个小地球仪,切换成中文输入,就能在模拟器中输入中文了。
coredata 删除数据
先来添加一个删除提示框,首先创建一个控制显示提示框的变量和用于存储被删除数据id的变量。
@State private var isDelete = false
@State private var deleteId: UUID = UUID()
// ...
然后添加删除按钮和提示框,保存被删除数据的 id。
if userList.isEmpty {
Text("暂无用户数据")
} else {
ForEach(userList, id: \.self) { item in
HStack {
Text(item.name)
Text("爱好:\(item.hobby)")
Button(action: {
isDelete = true
// 点击删除时保存要删除的数据的 id
self.currentUserId = item.id
}, label: {
Text("删除")
})
.alert("提示", isPresented: $isDelete) {
Button(role: .cancel) {
isDelete = false
} label: {
Text("取消")
}
Button(role: .destructive) {
} label: {
Text("删除")
}
} message: {
Text("确定删除吗?")
}
}
}
}
添加删除函数,该函数根据传递的 id 参数找到需要被删除的数据,然后传递给 viewContext.delete 函数,删除后保存即可。
func deleteUser(id: UUID) {
if let record = userList.first(where: { $0.id == id }) {
withAnimation {
viewContext.delete(record)
do {
try viewContext.save()
isDelete = false
} catch {
isDelete = false
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
调用 deleteUser 函数:
Button(role: .destructive) {
deleteUser(id: currentUserId)
} label: {
Text("删除")
}
在这里我已经事先新增了3条数据,来看看完成的效果:
coredata 修改数据
修改时候会用到上一步创建的表单和数据。
再新增一个变量控制修改:
@State private var showUpdate = false
在文字旁边新增一个修改按钮,同时将当前要修改的数据对象的值赋值给表单绑定值,让表单显示对应数据。
Text(item.name)
Text("爱好:\(item.hobby)")
Button(action: {
showUpdate = true
// 赋值
self.name = item.name
self.hobby = item.hobby
self.currentUserId = item.id
}, label: {
Text("修改")
})
当 showUpdate 为 true 时显示表单,以及切换为调用 updateUser 函数:
if showAdd || showUpdate {
VStack {
TextField("用户名", text: $name)
.padding()
.border(Color.gray)
TextField("爱好", text: $hobby)
.padding()
.border(Color.gray)
Button(action: {
if showAdd {
addUser()
} else if showUpdate {
updateUser(id: currentUserId)
}
}, label: {
Text("保存")
.padding(.horizontal, 2)
.padding(.vertical, 15)
.frame(maxWidth: .infinity)
.background(Color.blue)
.foregroundColor(.white).cornerRadius(24)
})
}.padding()
}
编写 updateUser 函数:
func updateUser(id: UUID) {
if let record = userList.first(where: { $0.id == id }) {
withAnimation {
record.name = self.name
record.hobby = self.hobby
do {
try viewContext.save()
showUpdate = false
} catch {
showUpdate = false
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
来看最后效果:
在上面的几个操作中,省略了一些数据清理工作,比如新增后的数据置空,表单的隐藏等。
总结
本文我们完成了 coredata 的增删改查等基本操作,相信经过学习你已经基本掌握了这些内容,coredata 还有很多使用方式,比如更细粒度的查询,分页等操作,有机会我们再讲吧。
这是 SwiftUI 开发之旅专栏的文章,是 swiftui 开发学习的经验总结及实用技巧分享,欢迎关注该专栏,会坚持输出。同时欢迎关注我的个人公众号 @JSHub:提供最新的开发信息速报,优质的技术干货推荐。或是查看我的个人博客:Devcursor。
?点赞:如果有收获和帮助,请点个赞支持一下!
?收藏:欢迎收藏文章,随时查看!
?评论:欢迎评论交流学习,共同进步!