1.Your First Core Data App¶
欢迎来到Core Data!
在本章中,您将编写第一个Core Data应用程序。您将看到使用Xcode中提供的所有资源(从起始代码模板到数据模型编辑器)是多么容易。
从一开始你就能顺利完成任务。在本章结束时,您将了解如何:
使用Xcode的模型编辑器对数据建模-向核心数据添加新记录-从核心数据获取一组记录-使用表视图显示获取的记录。
您还将了解Core Data在幕后做什么,以及如何与各种移动部件进行交互。这将使您更好地理解接下来的两章,这两章将继续介绍核心数据,并提供更高级的模型和数据验证。
入门¶
打开Xcode,根据 App 模板新建一个iOS项目。 将应用命名为 HitList,界面由 SwiftUI 改为 Storyboard。 本书中的所有示例项目都使用了Interface Builder和UIKit。 最后,确保勾选 Use Core Data。 确保未勾选 Host in CloudKit 和 Include Test。

勾选 Use Core Data 会导致Xcode为 AppDelegate.swift 中的NSPersistentContainer生成样板代码。
NSPersistentContainer由一组对象组成,这些对象便于从Core Data中保存和检索信息。 在这个容器中有一个对象来管理整个Core ata状态,还有一个对象表示DataModel,等等。
你将在前几章中了解这些作品。 之后,您甚至有机会编写自己的Core Data堆栈! 标准堆栈适用于大多数应用,但根据您的应用及其数据要求,您可以自定义堆栈以提高效率。
这个示例应用程序的想法很简单:将有一个表视图与您自己的“hit list”的名称列表。 您可以将名称添加到此列表中,并最终使用Core Data确保数据在会话之间存储。
在这本书中我们不宽恕暴力,所以你可以把这个应用程序看作是一个收藏夹列表,当然也可以跟踪你的朋友!
单击 main.storyboard,在界面生成器中打开。 在画布上选择视图控制器并将其嵌入到导航控制器中。 在Xcode的 Editor 菜单中,选择 Embed In… ▸ Navigation Controller。

接下来,从对象库中拖动一个 Table View 到视图控制器中,然后调整其大小,使其覆盖整个视图。
如果尚未打开,请使用画布左下角的图标打开Interface Builder的文档大纲。
按住Ctrl键从文档大纲中的 Table View 拖动到其父视图中,选择 Leading Space to Safe Area 约束:

再重复三次,选择约束条件 Trailing Space to Safe Area、Top Space to Safe Area,最后选择 Bottom Space to Safe Area。 添加这四个约束将使表视图填充其父视图。
Note
确保“界面生成器”创建的约束的常量为0点,这意味着与屏幕的每一侧齐平。 如果不是这种情况,您可以打开右侧的“大小检查器”并更正间距。
接下来,拖动一个 Bar Button Item 并将其放置在视图控制器的导航栏上。 最后选中条形按钮项,将其系统项改为 Add。
您的画布应类似于以下屏幕截图:

每次点击 Add 按钮,都会出现一个包含文本字段的提醒控制器。 从那里,你将能够键入某人的名字到文本字段。 点击Save将保存名称,关闭警报控制器并刷新表格视图,显示您输入的所有名称。
但首先,您需要使视图控制器成为表视图的数据源。 在画布中,按住Ctrl键从表格视图拖动到导航栏上方的黄色视图控制器图标上,如下图所示,点击 dataSource:

如果你想知道,你不需要设置表格视图的代理,因为点击单元格不会触发任何操作。 没有比这更简单的了!
通过按Control-Command-Option-Enter或选择“脚本”场景右上角的“调整编辑器”按钮并选择“助手”,打开助手编辑器,如下所示。

从表视图中按住Ctrl键拖动到类定义内的 ViewController.swift 上,创建一个IBOutlet:

接下来,将新的IBOutlet属性命名为tableView,结果如下:
@IBOutlet weak var tableView: UITableView!
接下来,按住Ctrl键,从 Add 按钮拖动到viewDidLoad()定义正下方的 ViewController.swift 中。 这一次,创建一个action而不是outlet,将方法命名为addName,类型为UIBarButtonItem:
@IBAction func addName(_ sender: UIBarButtonItem) {
}
现在可以在代码中引用表视图和条形按钮项的操作。
接下来,您将为表视图设置模型。 在tableView的IBOutlet下面的 ViewController.swift 中添加如下属性:
var names: [String] = []
names是一个可变的数组,它保存了表视图显示的字符串值。
接下来,将viewDidLoad()的实现替换为以下内容:
override func viewDidLoad() {
super.viewDidLoad()
title = "The List"
tableView.register(UITableViewCell.self,
forCellReuseIdentifier: "Cell")
}
这将在导航栏上设置一个标题,并将UITableViewCell类注册到表视图中。
Note
register(_:forCellReuseIdentifier:)保证当 Cell 的reuseIdentifier提供给dequeue方法时,您的表视图将返回正确类型的单元格。
接下来,仍然在 ViewController.swift 中,在ViewController的类定义下面添加如下UITableViewDataSource扩展:
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell =
tableView.dequeueReusableCell(withIdentifier: "Cell",
for: indexPath)
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
如果你曾经使用过UITableView,这段代码应该看起来很熟悉。 首先,您返回表中的行数作为names数组中的项数。
接下来,tableView(_:cellForRowAt:)将表视图单元格出队,并使用names数组中的相应字符串填充它们。
接下来,您需要一种添加新名称的方法,以便表视图可以显示它们。 实现之前用Ctrl拖动到代码中的addName的IBAction方法:
// Implement the addName IBAction
@IBAction func addName(_ sender: UIBarButtonItem) {
let alert = UIAlertController(title: "New Name",
message: "Add a new name",
preferredStyle: .alert)
let saveAction = UIAlertAction(title: "Save",
style: .default) {
[unowned self] action in
guard let textField = alert.textFields?.first,
let nameToSave = textField.text else {
return
}
self.names.append(nameToSave)
self.tableView.reloadData()
}
let cancelAction = UIAlertAction(title: "Cancel",
style: .cancel)
alert.addTextField()
alert.addAction(saveAction)
alert.addAction(cancelAction)
present(alert, animated: true)
}
每次点击 Add 按钮,此方法都会显示一个UIAlertController,包含一个文本字段和两个按钮:Save 和 Cancel。
Save 将文本字段当前文本插入names数组,然后重新加载表视图。 由于names数组是支持表视图的模型,因此您在文本字段中键入的任何内容都将显示在表视图中。
最后,第一次构建并运行您的应用程序。 然后点击 Add 按钮。 警报控制器看起来像这样:

在名单上加上四五个名字。 您应该会看到类似下面的内容:

您的表视图将显示数据,而您的数组将存储名称,但这里缺少的最重要的东西是 persistence。 阵列在内存中,但如果你强制退出应用程序或重新启动设备,你的命中列表将被清除。 核心数据提供持久性,这意味着它可以以更持久的状态存储数据,因此它可以在应用程序重新启动或设备重新启动后继续存在。
您尚未添加任何Core Data元素,因此在您导航离开应用程序后,应该没有任何内容会持续存在。让我们来测试一下。 如果您使用的是物理设备,请按Home按钮;如果您使用的是模拟器,请按等效按钮(Shift + ⌘ + H)。 这将带您回到主屏幕上熟悉的应用程序网格:

在主屏幕中,点击 HitList 图标,将应用返回到前台。 名字还在屏幕上。 发生了什么事?
当您轻按主屏幕按钮时,当前位于前台的应用程序将转到后台。 当这种情况发生时,操作系统会快速冻结内存中的所有内容,包括names数组中的字符串。 类似地,当需要唤醒并返回前台时,操作系统会恢复内存中的内容,就好像您从未离开过一样。
苹果在iOS 4中引入了多任务处理的这些进步。 它们为iOS用户创造了无缝的体验,但为iOS开发人员的持久性定义增加了一个皱纹。 这些名字真的存在吗?
不,不太想。 如果你在快速应用程序切换器中完全杀死了应用程序或关闭了手机,那些名字就会消失。 您也可以验证这一点。 在应用程序处于前台的情况下,进入快速应用程序切换器。
您可以通过双击Home按钮(如果您的设备有Home按钮)或从屏幕底部缓慢向上拖动(如果您使用的是iPhone X或更高版本)来执行此操作。
从这里,向上轻弹HitList应用程序快照以终止应用程序。从应用程序切换器中删除应用程序后,生活记忆中应该没有HitList的痕迹(没有双关语)。 通过返回主屏幕并点击HitList图标以触发新的启动,验证名称是否消失。
如果你已经使用iOS一段时间,并且熟悉多任务处理的工作方式,那么闪光冻结和持久化之间的区别可能是显而易见的。 然而,在用户的心目中,没有区别。 用户并不关心为什么这些名字还在那里,是否应用程序进入后台并返回,或者因为应用程序保存并重新加载它们。 所有重要的是,当应用程序回来的时候,名字还在!
因此,持久性的真实的测试是在新的应用程序启动后,您的数据是否仍然存在。
数据建模¶
现在您知道如何检查持久性,您可以深入了解Core Data。 你的HitList应用程序的目标很简单:保留您输入的名称,以便在新的应用程序启动后可以查看它们。
到目前为止,您一直在使用普通的旧Swift字符串将名称存储在内存中。 在本节中,您将用Core Data对象替换这些字符串。 第一步是创建一个 managed object model,它描述了Core Data在磁盘上表示数据的方式。
默认情况下,Core Data使用SQLite数据库作为持久性存储,因此您可以将数据模型视为数据库模式。
Note
你会在这本书中经常遇到“Managed”这个词。 如果在类的名称中看到“managed”,例如在NSManagedObjectContext中,则很可能您正在处理Core Data类。“Managed”是指Core Data对Core Data对象生命周期的管理。
但是,不要假设所有的Core Data类都包含单词“managed”。 大多数人不知道 有关Core Data类的完整列表,请查看文档浏览器中的Core Data框架参考。
由于您选择使用Core Data,Xcode会自动为您创建一个Data Model文件,并将其命名为 HitList.xcdatamodeld:

打开 HitList.xcdatamodeld。 正如你所看到的,Xcode有一个强大的数据模型编辑器:

数据模型编辑器有很多特性,您将在后面的章节中探索。 现在,让我们专注于创建一个单一的核心数据实体。
点击左下方 Add Entity,新建实体。 双击新实体,将其名称更改为 Person,如下所示:

你可能想知道为什么模型编辑器使用术语Entity。 你不是简单地定义了一个新类吗? 您很快就会看到,Core Data有自己的词汇表。 以下是一些你经常会遇到的术语的简要介绍:
- entity 是Core Data中的类定义。 典型的例子是
Employee或Company。 在关系数据库中,实体对应于表。 - attribute 是附加到特定实体的一条信息。 例如,
Employee实体可以具有雇员的name、position和salary的属性。 在数据库中,属性对应于表中的特定字段。 - relationship 是多个实体之间的链接。 在核心数据中,两个实体之间的关系称为 to-one relationships,而一个和多个实体之间的关系称为 to-many relationships。 例如,
Manager可以与一组员工建立 to-many relationship,而单个Employee通常与其经理建立 to-one relationship。
Note
您可能已经注意到实体听起来很像类。 同样,属性和关系听起来很像属性。 有什么区别? 您可以将核心数据实体视为类定义,将托管对象视为该类的实例。
现在你知道什么是属性了,你可以给前面创建的Person对象添加一个属性。 仍然在 HitList.xcdatamodeld 中,选择左侧的Person,然后单击 Attributes 下的加号(+)。
设置新属性的名称为,呃,name,类型为 String:

在Core Data中,属性可以是几种数据类型之一。 你将在接下来的几章中了解这些。
保存到核心数据¶
打开 ViewController.swift,在import UIKit下面添加如下Core Data模块导入:
import CoreData
这个导入是您开始在代码中使用Core Data API所需的全部内容。
接下来,将names属性定义替换为以下内容:
var people: [NSManagedObject] = []
您将存储Person实体而不是字符串名称,因此将用作表视图数据模型的数组重命名为people。 它现在包含NSManagedObject的实例,而不是简单的字符串。
NSManagedObject表示存储在核心数据中的单个对象;您必须使用它来创建、编辑、保存和删除您的核心数据持久存储。 您很快就会看到,NSManagedObject是一个变形器。 它可以采用数据模型中的任何实体的形式,占用您定义的任何属性和关系。
由于要更改表视图的模型,因此还必须替换前面实现的两个数据源方法。 将UITableViewDataSource扩展替换为以下内容:
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return people.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let person = people[indexPath.row]
let cell =
tableView.dequeueReusableCell(withIdentifier: "Cell",
for: indexPath)
cell.textLabel?.text =
person.value(forKeyPath: "name") as? String
return cell
}
}
对这些方法最重要的更改发生在tableView(_:cellForRowAt:)中。 现在,您将单元格与对应的NSManagedObject进行匹配,而不是将单元格与模型数组中对应的字符串进行匹配。 请注意如何从NSManagedObject获取name属性。 它发生在这里:
cell.textLabel?.text =
person.value(forKeyPath: "name") as? String
你为什么非要这么做 事实证明,NSManagedObject并不知道您在数据模型中定义的name属性,因此无法通过属性直接访问它。 Core Data提供的读取值的唯一方法是 key-value coding,通常称为KVC。
Note
KVC是Foundation中使用字符串间接访问对象属性的机制。 在这种情况下,KVC使NSMangedObject在运行时表现得有点像字典。
所有从NSObject继承的类,包括NSManagedObject,都可以使用键值编码。 你不能在Swift对象上使用KVC访问属性,因为它不是从NSObject派生而来的。
接下来,找到addName(_:),并将save的UIAlertAction替换为以下内容:
let saveAction = UIAlertAction(title: "Save", style: .default) {
[unowned self] action in
guard let textField = alert.textFields?.first,
let nameToSave = textField.text else {
return
}
self.save(name: nameToSave)
self.tableView.reloadData()
}
这将获取文本字段中的文本,并将其传递给名为save(name:)的新方法。 Xcode会抱怨,因为save(name:)还不存在。 在addName(_:")下面添加如下实现:
func save(name: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
// 1
let managedContext =
appDelegate.persistentContainer.viewContext
// 2
let entity =
NSEntityDescription.entity(forEntityName: "Person",
in: managedContext)!
let person = NSManagedObject(entity: entity,
insertInto: managedContext)
// 3
person.setValue(name, forKeyPath: "name")
// 4
do {
try managedContext.save()
people.append(person)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
这就是核心数据发挥作用的地方! 下面是代码的作用:
- 在您可以从
Core Data存储中保存或检索任何内容之前,您首先需要获得NSManagedObjectContext。 您可以将托管对象上下文视为内存中的“scratchpad”,用于处理托管对象。 将一个新的托管对象保存到Core Data可以分为两步:首先,将新的托管对象插入托管对象上下文;一旦你满意了,你就“commit”了你的托管对象上下文中的更改以将其保存到磁盘。Xcode已经生成了一个托管对象上下文作为新项目模板的一部分。 请记住,只有在开始时选中 Use Core Data 复选框时才会发生此情况。 此默认托管对象上下文作为应用程序委托中的NSPersistentContainer的属性存在。 要访问它,首先要获取对应用程序委托的引用。 - 创建一个新的托管对象并将其插入到托管对象上下文中。 您可以使用
NSManagedObject的静态方法一步完成此操作:entity(forEntityName:in:)。 您可能想知道NSEntityDescription是什么。 回想一下前面,NSManagedObject被称为shape-shifter类,因为它可以表示任何实体。 实体描述是在运行时将数据模型中的实体定义与NSManagedObject的实例链接起来的部分。 - 有了
NSManagedObject,您就可以使用键值编码来设置name属性。 您必须将KVC密钥(在本例中为name)exactly 拼写为数据模型中的密钥,否则您的应用将在运行时崩溃。 - 您可以通过在托管对象上下文中调用
save将更改提交给person并保存到磁盘。 注意保存可能会抛出错误,这就是为什么在do-catch块中使用try关键字调用它的原因。 最后,将新的托管对象插入到people数组中,以便在重新加载表视图时显示它。
这比使用字符串数组要复杂一点,但也不算太糟。 这里的一些代码,例如获取托管对象上下文和实体,可以在您自己的init()或viewDidLoad()中只执行一次,然后在以后重用。 为了简单起见,您将使用相同的方法完成所有操作。
构建并运行应用程序,并向表视图添加一些名称:

如果名称实际上存储在Core Data中,则HitList应用程序应该通过持久性测试。 将应用程序置于前台,转到快速应用程序切换器,然后终止它。
从Springboard,点击HitList应用程序以触发新的启动。 等等发生什么事了?表视图为空:

您已保存到Core Data,但在新的应用程序启动后,people数组为空! 这是因为数据就在磁盘上等着你,但你还没有显示出来。
取核心数据¶
要将数据从持久化存储中获取到托管对象上下文中,需要 fetch 它,打开 ViewController.swift,在viewDidLoad()下面添加以下内容:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//1
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
//2
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Person")
//3
do {
people = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
一步一步地,这就是代码所做的:
- 在使用
Core Data做任何事情之前,您需要一个托管对象上下文。 捡东西也不一样! 像前面一样,您拉出应用程序委托并获取对其持久容器的引用,以获得其NSManagedObjectContext。 - 顾名思义,
NSFetchRequest是负责从Core Data中获取数据的类。 获取请求既强大又灵活。 您可以使用fetch请求来获取一组满足所提供的条件的对象(即,给予我所有居住在威斯康星州并且在该公司至少工作了三年的员工)、个人值(即, 给予我数据库中最长的名字)等等。 获取请求有几个限定符用于细化返回的结果集。 您将在第4章“中间获取”中了解更多关于这些限定符的内容;现在,您应该知道NSEntityDescription是这些必需的限定符之一。 设置获取请求的entity属性,或者使用init(entityName:)初始化它,获取特定实体的所有对象。 这就是你在这里做的,以获取所有的“Person”实体。 另外,请注意NSFetchRequest是一个泛型类型。 泛型的这种使用指定了获取请求的预期返回类型,在本例中为NSManagedObject。 - 您将获取请求交给托管对象上下文来完成繁重的工作。
fetch(_:)返回符合fetch请求指定条件的托管对象数组。
Note
和save()一样,fetch(_:)也会抛出错误,所以你必须在do块中使用它。 如果在获取过程中发生错误,您可以在catch块中检查错误并做出适当的响应。
生成并运行应用程序。 您应该立即看到您之前添加的名称列表:

太好了! 他们死而复生了(双关语)。 向列表中添加更多名称,然后重新启动应用程序以验证保存和提取是否正常。 除了删除应用程序,重置模拟器或将手机从高楼上扔下来之外,无论如何,这些名称都会出现在表格视图中。
Note
在这个示例应用程序中有一些粗糙的边缘:每次都必须从应用程序委托中获取托管对象上下文,并且使用KVC来访问实体的属性,而不是更自然的对象风格的person.name。
有更好的方法可以从Core Data中保存和获取数据,您将在后面的章节中探索。 在这里做“长路”的目的是了解幕后发生了什么!
关键点¶
Core Data提供 on-disk persistence,这意味着即使在终止应用或关闭设备后,您的数据也可以访问。 这与内存中持久性不同,内存中持久性只会在应用程序位于内存中时保存数据,无论是在前台还是在后台。Xcode自带强大的 Data Model editor,您可以使用它创建您的 managed object model。- 托管对象模型由 entities、attributes 和 relationships 组成
- entity 是
Core Data中的类定义。 - attribute 是实体附加的一条信息。
- relationship 是多个实体之间的链接。
NSManagedObject是核心数据实体的运行时表示。 您可以使用 Key-Value Coding 读写其属性。- 您需要
NSManagedObjectContext来save()或fetch(_:)数据到Core Data或从Core Data获取数据。