原文:Getting Started with Core Data Tutorial
作者:Pietro Rea
译者:kmyhy
这是《Core Data by Turoials》一书的缩略章节,目前已经升级至 Swift 4 和 iOS 11。值得高兴的是,本教程被作为 iOS 11 Lanuch Party 中的一部分放出。
欢迎来到 Core Data 的世界!在本教程中,你将编写一个非常简单的 Core Data app。你会看到通过 Xcode 提供的一系列工具,比如开始的模板代码和 Data Model 编辑器,我们能够轻易上手。
我们将马上开始。
当本教程结束,你将学会:
你也会了解 Core Data 在底层做的工作,以及如何和各种 moving pieces 打交道。
开始
打开 Xcode ,新建 iOS 项目,模板使用 Single View App 。项目名称命名为 HitList,勾上 Use Core Data 选项。
勾选 Use Core Data 选项将导致 Xcode 生成模板代码,也就是 AppDelegate.swift 中的 NSPersistentContainer。
NSPersistentContainer 包含了一系列用于在 Core Data 中保存和检索数据的对象。在这个容器中,有负责将 Core Data 状态作为一个整体进行管理的对象,也有代表数据模型的对象,等等。
对于大部分 app 来说,这种标准栈足以使用,但根据 app 和它的数据需求的不同,你可以定制这个栈以获得更高的效率。
注意:在 iOS/Application 模板下,不是所有的 Xcode 模板都可以使用 Core Data。在 Xcode 9 中,只有 Master-Detail App 和 Single View App 模板拥有 Core Data 选项。
这个示例 app 的功能很简单:有一个 table view,展示一个你自己的“黑名单”列表。你可以在列表中添加名字,然后保存到 Core Data 中,确保数据在会话之间不被丢失。这本书当然不会宣扬暴力,因此你可以把 app 当成一个记录你的朋友喜好的列表。
点击 Main.storyboard,打开 IB。
在画布中选中这个 view controller,将它用 navigation controller 包装起来。在 Xcode 的 Editor 菜单中,选择 Embed In…\Navigation Controller。
点击 navigation controller 的导航栏,然后在属性检查器中点击 Prefers Large Tittles。这将使 app 显示为 iOS 11 风格。
然后,从 Object Library 拖一个 Table View 到 view controller 中,然后修改大小为整个 view 大小。
如果 Document Outline 窗口未打开,请点击画布左下角的图标打开它。
右键,从 document outline 中的 Table View 拖一条线到它的父 view,并选择 Leading Space to Safe Area:
重复这个动作 3 次,分别选择 Trailing Space to Safe Area,Top Space to Safe Area 和 Bottom Space to Safe Area。这 4 个约束将使 table view 填充整个父视图。
接着,拖一个 Bar Button Item 到 View controller 的导航条上。最后,选择 bar button item,将它的 system item 修改为 Add。你的画布会变成这个样子:
当你点击 Add 按钮,会显示一个 alert controller。你可以在它的 text field 中输入一个人的名字。点击 Save,保存这个人名,alert 消失,table view 会刷新,显示出你输入的名字。
但首先,你必须将 View controller 设置 table view 的数据源。在画布中,右键点击 table view 拖一条线到导航条的黄色 view controller 图标上,然后点击 dataSource,如下图所示:
你可能奇怪,为什么不设置 table view 的 delegate,因为点击 cell 时我们不需要触发任何动作。这再简单不过了!
按下 Command-Option-Enter,或者点击 Xcode 工具栏上 Editor 工具中间的按钮,打开助手编辑器。删除 didReceiveMemoryWarning()方法。然后,右键,从 table view 拖到 ViewController.swift 的类定义中,创建一个 IBOutlet。
然后,为 IBOutlet 属性取名为 tableView,这会添加一句代码:
@IBOutlet weak var tableView: UITableView!
然后,右键,从 Add 按钮拖一条线到 ViewController.swift 的 viewDidLoad() 方法以下。这次,会创建一个 IBAction 而不是 IBOutlet,命名为 addName,Type 栏选择 UIBarButtonItem:
@IBAction func addName(_ sender: UIBarButtonItem) {
}
现在,你可以在代码中引用 table view 和 bar button item 的 action 了。
然后,为 table view 创建模型。在 ViewController.swift 的 tableView 属性声明后添加一个属性:
var names: [String] = []
names 属性是一个可变数组,用于保存要显示在 table view 中的字符串。接着,将 viewDidLoad() 方法修改为:
override func viewDidLoad() {
super.viewDidLoad()
title = "The List"
tableView.register(UITableViewCell.self,forCellReuseIdentifier: "Cell")
}
这将设置 navigation bar 的标题,并注册 UITableViewCell 类到 table view。
注意:register(_:forCellReuseIdentifier:) 允许你在以 Cell 为 ID 调用 dequeue 方法时,返回正确的 cell 类型。
继续在 ViewController.swift 中,添加一个 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:) 方法从缓存中获得一个 cell,然后用 names 数组中对应的字符串渲染 cell。
接着,我们需要添加新的名字到表格中去显示。实现前面通过右键拖到代码中的 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: .default)
alert.addTextField()
alert.addAction(saveAction)
alert.addAction(cancelAction)
present(alert,animated: true)
}
当你点击 Add 按钮,这个方法会弹出一个带文本框和两个按钮 Save 、Cancel 的 UIAlertController。
Save 按钮将文本框中的文字保存到 names 数组,然后重新加载 table view。因为 names 数组是 table view 的模型,所以你输入到文本框中的文本会显示在 table view 里。
Build & run。点击 Add 按钮,alert 将显示:
添加 4、5 个名字到列表中。你会看到:
table view 中能够显示这些数据,数组也会保存所有名字,但却没有持久化。数组只是放在了内存里,但如果你退出 app,重启设备,这些数据都会丢失。
Core Data 提供了持久化,它能将数据保存在一种耐久状态,以便数据在 app 重启或设备重启后不被丢失。
我们还没有添加任何 Core data,因此从 app 离开后任何东西都不会保存。我们来试试看。如果你使用真机调试,按下 Home 键,如果使用模拟器,则按下 shift+command+H 键,回到熟悉的 Home 界面。
在 Home 界面,点击 HitList 图标启动 app 进入前台。names 数组仍然在屏幕上显示。为什么呢?你按 Home 按钮时,app 从前台进入后台。这时,操作系统会将内存中的一切进行缓存,包括 names 数组中的字符串。 同样,当它从后台返回前台,操作系统会恢复内存中的数据,就好像你从来没有离开一样。
苹果从 iOS4 开始引入了这些多任务的高级属性。它们向 iOS 用户提供了一种无缝的体验,但是给 iOS 开发者添加了一种持久化持久化的假象。但是 names 数组真的被持久化了吗?
答案是不。如果你完全杀死 app 进程或者手机关机,names 数组中将什么都没有了。你可以试一下。当 app 在前台时,双击 Home 按钮,进入快速 app 切换工具:
在这个界面中,将 HitList APP 的截图向上划,终止 app 进程。这将让 HitList 从内存中移除。回到 Home 界面,点击 HitList,再次打开 app,你会发现 names 数组被清空了。
缓存到内存和持久化是两个截然不同的概念,如果你熟悉 iOS 和多任务的工作方式的话。只不过在用户的眼中,它们没有区别。用户不关心为什么 names 仍然还在,到底是 app 进入后台又返回前台,还是 app 保存了它们后重新加载。
总之,重点在于当 app 返回之后,names 数组仍然存在!
因此,要真正测试是否持久化,需要重新启动 app 之后再看你的数据是否仍然存在。
建立数据模型
现在你知道如何判断有没有持久化了,我们可以开始讲 Core Data 了。我们的目的很简单:对你输入的名字进行持久化,以便当 app 重启后仍然可以看到这些数据。
到目前为止,你还在用简单的古老的 Swift 字符串方式将 names 保存在内存里。在这一节,你会用 Core Data 对象来代替这些字符串。
首先要创建一个托管对象模型,这是 Core Data 用于描述存储在磁盘上的数据的方式。
默认,Core Data 使用 sqlite 数据库来进行持久化,因此你可以把数据模型看成是数据库中 schema 的概念。
注意:在本书中,始终贯穿了“托管”一词。如果你在某个类的类名中看到“managed”字样,比如 NSManagedObjectContext,你可以认为它是一个 Core Data 类。“managed”表示 Core Data 会负责管理这个 Core Data 对象的生命周期。
当然,不是所有的 Core Data 类的名字都包含有“managed”。实际上,大多数都没有。要查看 Core Data 类列表,请阅读文档浏览器中的 Core Data 框架手册。
因为你勾选了 use Core Data,Xcode 会自动创建一个 Data Model 文件并命名为 HitList.xcdatamodeld.
打开 HitList.xcdatamodeld。你会看到,Xcode 的强大的数据模型编辑器打开了:
数据模型编辑器有许多功能,但目前我们只需要知道怎么创建单个 Core Data 实体就可以了。
点击左下角的 Add Entity,创建一个新的实体。双击这个新实体,修改它的名字为 Person:
你也许奇怪,为什么模型编辑器会使用“实体”一词。为什么不是简单地用类来表示?你后面就知道了,Core Data 拥有自己的一套术语。其中比较常见的会有这几个:
- 实体 entity 是 Core Data 中的类的概念。例如 Employee 或者 Company。在关系型数据库中,实体对应了表。
- 属性 attribute 表示某个实体中的一个信息片段。例如,对于一个 Employee 实体来说,可能包含员工姓名、职务和工资属性。在数据库中,一个属性就对应表中的字段。
- 关系 relationship 表示多个实体之间的连接。在 Core Data,两个实体之间关系叫做 1 对 1 关系,一个实体和多个实体之间的关系叫做 1 对多关系。例如,一个经理和多个下属员工之间存在“1对多”关系,而一个员工通常只会和他的经理存在“1对1”关系。
注意:你可能觉得实体看起来就像是类。属性和关系和类的属性也有点像。那它们之间有什么不同?你可以把一个 Core Data 实体看成是一个类的定义,而托管对象则是那个类的一个实例。
知道了属性是什么之后,你可以为 Person 对象添加一个属性了。打开HitList.xcdatamodeld。然后,选择左侧的 Person 对象,点击 Attributes 下面的 + 按钮。
将新属性的名字设置为 name,类型则改为 String。
保存到 Core Data
打开 ViewController.swift,在 UIKit 的导入语句之后加入 Core Data 的导入语句:
import CoreData
这个 import 语句使你能够在代码中使用 Core Data API。
var people: [NSManagedObject] = []
我们准备用 Person 实体而不是 String 来保存数据,因此我们将 table view 的数据模型的名字修改为 people。它现在保存的是 NSManagedObject 实例而不是简单字符串。
NSManagedObject 代表了一个 Core Data 中存储的对象,你必须用它来创建、编辑、保存和删除 Core Data 持久化存储中的数据。待会你会看到,NSManagedObject 是一种变形动物。它可以用于表示你的数据模型中的任意实体,自动适配你所定义的任意属性和关系。
因为你修改了 table view 的模型,你必须同时修改数据源方法。将你的 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:) 方法。原来设置 cell 时用的是模型数组中的字符串,现在改成了 NSManagedObject。
注意如何从 NSManagedObjectd 中获取 name 属性。也就是这句:
cell.textLabel?.text = person.value(forKeyPath: "name") as? String
为什么呢?前面说过,NSManagedObject 并不知道你在数据模型中定义了一个 name 属性,因此你没有办法直接通过属性来访问 name。Core Data 只提供键值编码的方式,也就是 KVC。
注意:KVC 是 Foundation 框架中的一种机制,用于通过字符串的方式直接访问对象属性。这里,KVC 使得 NSManagedObject 在运行时变成了某种程度上的字典。键值编码对任何继承自 NSObject 的类都是有效的,包括 NSManagedObject。你不能对没有继承自 NSObject 的 Swift 对象使用 KVC。
接着,找到 addName(_:) 方法,将保存按钮的 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()
}
去除 text field 中的文本,将它传递到 save(name:) 方法中。xcode 会报错,因为 save(name:) 方法还没有写。新增一个方法:
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。你可以把它看成是一个位于内存中的托管对象的“暂存器”。
将一个新的托管对象保存到 Core Data 需要两个步骤:首先插入一个新的托管对象到托管对象上下文,在拥有了崭新的托管对象之后,将你对它所做的改变提交到上下文,即可保存到磁盘。
Xcode 已经生成了一个托管对象上下文,就在新建项目模板的时候。注意,只有在你勾选了 Use Core Data 选项之后才会有这个上下文。默认,这个上下文会声明做 application delegate 的一个属性。要访问它,你首先要获得对 app delegate 的一个引用。
创建一个新的托管对象,并插入到托管对象上下文中。你可以用 NSManagedObject 的静态方法 entity(forEntityName:in:)。
你可能不知道 NSEntityDescription 是什么东东。回想前面所说的,NSManagedObject 是一种万能类,因为它可以表示任意实体。一个实体描述(entity description)表示一种实体定义,将你的 Data Model 转换成运行时的 NSManagedObject 对象。
有了 NSManagedObject 之后,就可以用 KVC 来设置 name 属性了。你必须将 KVC 的 key 拼写得和数据模型中的一模一样,否则 app 会崩溃。
- 提交对 person 的修改,通过调用上下文对象的 save 方法保存到磁盘。注意,save 方法会抛出一个异常,因此我们需要用 do catch 块将 try 语句包裹起来。最后,插入这个新的托管对象到 people 数组,这样表格刷新时就会显示出来。
比起字符串数组来说,这是有点复杂,但算不得什么了。其中一些代码,比如获取上下文和实体,可以在你自己的 init() 方法或 viewDidLoad() 方法中只编写一次,然后重用。这里是为了简单,所以将全部代码都写在一个方法里。
Build & run,添加几个名字到表格里:
如果名字真滴保存到了 Core Data,HitList app 应该能够通过我们的持久化测试。双击 Home 键,打开快速 app 切换工具。向上划,终止 HitList app。在 Springboard 中,点击 HitList app 打开它。
咦?为什么 table view 变成空的了呢?
我们虽然保存了 Core Data,但 app 重启之后,people 数组仍然是空的!因为磁盘上的数据仍然待在那里,你并没有显示它们呀!
从 Core Data 中读取数据
要从持久化存储中读取数据到托管对象上下文,你必须自己去抓取它们。打开 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 之前,我们需要托管对象上下文。抓取数据也不例外!和之前相同,我们获取了 app delegate 然后获得它的持久化容器的引用,然后拿到它的 NSManagedObjectContext。
如同其名称所暗示的,NSFetchRequest 是一个用于从 Core Data 拉取数据的类。它很强大,也很灵活。你可以用它抓取一些列符合指定条件(比如“告诉我所有居住在 Wisconsin 并且入职公司至少 3 年的员工”)的对象,单个值(比如“告诉我数据库中谁的名字最长”),等等。
fetch request 有几个限定符,用于过滤返回的结果集。你可以在第4章“Intermediate Fetching”中进一步学习;就目前而言,你只需要知道在这些限定符中,NSEntityDescription 是必须的。
设置 fetch request 的 entity 属性,或者用 init(entityName:) 进行初始化,将拉取指定实体的所有对象。在这里,你抓取了所有的 Person 实体。注意 NSFetchRequest 是一个泛型。泛型可以指定 fetch request 的返回类型,例如这里的 NSManagedObject。将 fetch request 传递给上下文。fetch(_:) 方法返回一个符合查询条件的 NSManagedObject 数组。
注意:和 save() 一样,fetch(_:) 方法也会抛出一个异常,你比需在 do 块中调用它。如果抓取中出现错误,你可以在 catch 块中获得这个错误并进行处理。
Build & run,你立马就可以在列表中看到之前添加的名字了:
太好了!它们复活了(双关语)。添加几个名字,重启 app,检查能够正常保存和抓取数据。只要你不要删除 app、重置模拟器或者从高楼大厦上扔下你的 iPhone,这些名字都会显示。
接下来做什么
从这里下载完成后的项目。
在这几页内容中,你已经学习了 Core Data 中的几个基本概念:数据模型、实体、属性、托管对象、托管对象上下文以及 fetch request。
如果你喜欢本教程,那么请阅读完整的[ Core Data by Tutorials]一书。
这本书中你会学到:
- 第一章“你的第一个 Core Data App”:你将通过 File\New Project 来从头编写一个Core Data app!这一章将介绍如何创建数据模型,如果添加和抓取记录。
- 第二章“NSManagedObject 子类”:NSManagedObject 是 Core Data 类图中的数据存储类的基类。这一章将介绍如何自定义自己的 NSManagedObject 子类以存储和校验数据。
- 第三章“Core Data 组件”:Core Data 底层由几部分构成。在这一章,你将学习这几部分如何构成的,以及如何不通过 Xcode 的初始化模板来构建你自己的定制化的 app。
- 第四章“数据查询进阶”:你的 app 永远都需要抓取数据,Core Data 提供了各种高效的获取数据的选项。这一章涉及到许多高级的 fetch request 内容,比如谓词、排序和异步抓取。
- 第五章“NSFetchedResultsController”:在许多 app 中,TableView 都是必不可少的,苹果想让它和 Core Data 能更好地结合在一起。在这一章,你将学习当你用 Core Data 作为 table view 的数据源时,用 NSFetchedResultsController 来节省时间、减少代码。
第六章“版本和迁移”:当你升级和修改 app 时,几乎都会对数据模型做某种程度上的修改。在这一章,你将学习如何创建 data model 的多个版本,然后允许你的用户向前迁移,这样他们就能在升级的同时保持原有数据。
第七章“单元测试”:测试在开发中是一个重要部分,你的 Core Data 也不例外!在这一章,你将学习如何为 Core Data 创建单独的测试环境,以及几个测试你的模型的例子。
- 第八章“测试和提升性能”:没人会嫌弃一个 app 太快,因此要保持对性能的敏感性。在这一章,你将学习如何用各种 app 工具测试 app 性能,学会处理低性能代码的技巧。
- 第九章“多托管对象上下文”:最后一章,将在一般的 Core Data 组件之外增加一个多托管对象上下文。你将学习它为什么能够明显提升性能并让你的 app 结构更紧凑以及松耦合。
为了促销,该书的数字版仅售 49.99 美元!不要犹豫,这个价格只在短时间内有效。
提到促销,请一定要看看我们的年度大奖
iOS11 Launch Party,里面有超过了 9000 美金的奖品。
要参加这次抽奖,请点击下面的按钮用 #ios11lanuchparty 井号标签在 tweeter 中转发本文:
希望你喜欢本文,敬请关注更多书籍和更新!