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
获取数据。