Core Data 入门教程

原文:Getting Started with Core Data Tutorial
作者:Pietro Rea
译者:kmyhy

Core Data 欢迎你!在本教程中,你会编写你的第一个 Core Data app。你会发现利用 Xcode提供的工具编写这个 app 是如此的简单,这些工具包括了最初的模板代码到数据模型编辑器。

你将从头开始编写这个 app。学完本教程,你会学习到:

  • 用 Xcode 的模型编辑器进行数据建模
  • 向 Core Data中插入数据
  • 从 Core Data 中抓取数据
  • 用 table view 显示抓取到的数据

通过这种方式,你顺便也了解了 Core Data 在背后为我们所做的工作,以及如何和 Core Data 进行交互。

开始

打开 Xcode 新建一个 Single View Application。命名项目为 HitList 并勾选 Use Core Data 选项:

勾选 Use Core Data 将让 Xcode 为我们创建模板代码,比如在 AppDelegate.swift 中创建一个 NSPersistentContainer。

一个 NSPersistentContainer 包含了一系列用于保存、检索 Core Data 数据的对象。在这个容器中,有一个专门负责同步 Core Data 状态的对象,一个数据模型的对象,以及类似的对象。

这个标准的动作适用于大部分 app,但根据不同的 app 种类以及不同存储需求,为了更有效率,你可以自定义这些动作。

注意:不是所有的 iOS/Application 下的 Xcode 模板都有 Use Core Data 选项。在 Xcode 8 中,只有 Master-Detail Applicatoin 和 SingleView Application 模板有这个选项。

示例 app 的功能非常简单:有一个 table view 列出了所有关于你的“Hit list”的名称列表。你可以向列表中加入新的名称,最终用 Core Data 保存数据,这样就可以在不同的会话( app 从启动到退出)中共享数据。我们不喜欢暴力,你可以将这个 app 用来记录你的朋友的最喜欢去的网址 :]

点击 Main.storyboard,打开 IB。选中 View Controller,在它外面包裹一个 Navigation Controller。在 Xcode 的 Editor 菜单中,选择 Embed In…\ Navigation Controller。

从 Object Library 中拖一个 Table View 到 View Controller 中,将大小设置为占据整个视图。

如果还没打开 IB 的 document outline 窗口,点击画布左下角的按钮将其打开。

在 document outline 窗口中,用右键(ctrl+左键)从 TableView 拖一条线到它的父视图,然后选择 Leading Space to Container Margin:

重复此动作 3 次,分别选择 Trailing Space to Container Margin、 Vertical Spacing to Top Layout Guide和 Vertical Spacing to Bottom Layout Guide。这 4 个约束的目的是让 table view 占满它的父视图。

接着,拖一个 Bar Button Item 在 View Controller的导航栏上。选中这个 Bar Buton Item,将它的 system item 修改为 Add。现在你的画布是这个样子:

当你点击 Add 按钮时,会弹出一个带文本输入框的 Alert Controller。在这个输入框中你可以输入某个人的名字。然后点击 Save,将这个名字保存并解散 Alert Controller,刷新 Table View,将你输入的名字显示在列表中。

首先,你需要将 ViewController 配置成 Table View 的数据源。在画布上,右键从 Table View 拖到导航栏上的黄色的 View Controller 图标上,然后选择 dataSource:

你可能奇怪,为什么不设置 Table View 的 delegate,因为我们不需要在点击 cell 的时候触发什么动作。这已经是没法更简单了!

按下 command+option+回车,打开 Assistant Editor,你也可以点击 Xcode Editor 工具栏的中间的按钮。删掉 didReceiveMemoryWarning() 方法。然后用右键(ctrl+左键)从 Table View 拖一条线到 ViewController.swift 文件中的类定义之内的地方,创建一个 IBOutlet:

命名这个 IBOutlet 为 tableView,这回插入一条语句:

@IBOutlet weak var tableView: UITableView!

右键从 Add 按钮拖一条线到 ViewController.swift 的 viewDidLoad() 方法下边。这次创建一个 IBAction 而不是 IBOutlet,将方法命名为 addName,参数类型为 UIBarButtonItem:

@IBAction func addName(_ sender: UIBarButtonItem) {

}

现在 Table View 的引用已经创建,Bar Button Item 的动作也设置好了。

接下来创建 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")
}

这里给导航栏一个标题,并向 tableView 注册了一个 UITableViewCell类。

注意: register(_:forCellReuseIdentifier:) 方法确保 tableView 会返回正确类型的 cell,同时将 Cell 作为 cell 缓存的重用 ID,这个 ID 会在从 cell缓存中 dequeue 一个 cell 时用到。

仍然在 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 数组的个数来作为要显示的 Table View 的行数。

然后是 tableView(_:cellForRowAt:) 方法,这个方法会从缓存中取出一个 table view cells,并将 names 数组中对应的字符串显示在 cell 上。

我们还需要一个方法将新的名字添加到 names 数组,以便 table view 能显示它们。将早先创建的 addName 方法实现为如下代码:

// Implement the addName IBAction
@IBAction func addName(_ sender: AnyObject) {

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 按钮,这个方法会弹出一个 UIAlertController,这个 UIAlertController 中会带有一个文本编辑框和两个按钮:Save 按钮、 Cancel 按钮。

点击 Save 按钮会将文本框中的文字插入到 names 数组并刷新表格。因为 names 数组为表格提供了模型,所以你输入的文字将显示到表格上。

最后,第一次编译运行 app。点击 Add按钮,Alert Controller 显示出来:

添加 4-5 个名字到列表中。显示结果如下:

表格中显示的数据来自于 names 数组中存储的名字,但现在最大的问题是这些数据还没有持久化。数组是放在内存中的,如果你退出了 app,或者重启设备,列表中的数据就消失了。

Core Data 提供了持久化,它能够将内存中的数据保存下来,这样无论重新启动 app 还是重启设备,这些数据都会持久存在。

你现在还没有加入任何 Core Data 数据,所以当你离开 app 后没有任何数据被持久化。现在让我们来做个试验。如果你在使用真实设备,请按下 Home键,如果是使用模拟器,请按下 shift+command+H。这将返回到我们熟悉的列有 app 图标的网格式的 home 界面:

在 home 屏上,点开 HitList 图标,重新打开 app。列表中仍然列出了那些名字。这是为什么?

当你按下 Home 键时,app 从前台进入后台,这时,操作系统将内存中的内容 flash-freezing(速冻技术,临时保存到缓存),包括 names 数组。当 app 被唤醒,返回前台时,操作系统又将缓存的数据恢复到内存,这样就像你从来没有离开过这个 app 一样。

苹果从 iOS 4 开始使用这个多任务特性。他们为 iOS 用户创建了一种无缝式的体验,用类似于 iOS 开发者的持久化的概念。是 names 数组真的被持久化了吗

当然不。如果你使用 fast app switcher 工具杀死 app 进程,或者关掉手机,names 数组就真的回不来了。你可以测试一下。当 app在前台的时候,双击 Home 键进入 fast app switcher 工具:

然后,向上滑动 HitList app,这样就退出了 app。这回 HitList 彻底从生命的记忆中(即内存中,非双关语)消失了。返回 Home 屏,点开 HitList 图标,重新点开 app 后你会发现 names 列表不在了。

持久化和 flash-freezing 速冻技术的区别是很明显的,如果你曾经用过 iOS 并且对多任务不陌生的话。以用户的观点看,二者并无不同。当 app 退到后台又回到前台后,names 列表仍然存在到底是因为什么原因,用户不会关心到底是什么原因?是 app 做了数据保存并重新加载还是别的什么原因?

所以问题的关键就是,当 app 重新打开后 names 列表仍然存在!

所以持久化的衡量标准,就是 app 重启后数据是否仍然存在。

对数据进行建模

知道如何判断数据是否被持久化了吧?现在开始使用 Core Data。对于 HitList 来说目标很简单:持久化你输入的 names 列表,让它们在重启 app之后仍然能够显示出来。

目前,你仍然使用的是 Swift 中最传统的 String 来将 names 列表保存到内存中。在这一节,你将用 Core Data 对象来取代 String。

首先的一步,是创建一个托管对象模型,我们用它来描述 Core Data 对象在磁盘上的存储方式。

默认,Core Data 使用 SQLite数据库来存储数据,因此你可以把数据模型看成是一个数据库 schema。

注意:在 Core Data 中你将反复见到“managed”这个词。如果你在一个类名中看到 managed 字样,比如 NSManagedObjectContext,就说明你正在使用 Core Data 类。“Managed”在 Core Data 中表示 Core Data 负责对 Core Data 对象的生命周期进行管理。

当然,不是所有的 Core Data 类名中都包含 managed。实际上,大部分情况下都不包含 managed。完整的 Core Data 类列表,请参考 Core Data 框架手册。

因为你已经勾选了 Use Core Data,Xcode 会自动创建一个 Data Model 文件,名字就叫 HitList.xcdatamodeld。

打开 HitList.xcdatamodeld。如你所见,Xcode 内置了一个强大的 Data Model 编辑器:

现在,我们来创建一个 Core Data 实体。

点击左下角的 Add Entity 按钮,创建一个实体。双击这个新创建的实体,将名字改为 Person,如下所示:

为什么模型编辑器会使用“实体”一次。为什么不简单地定义一个新类?等下你会发现,Core Data 使用它自己的词汇。你经常会遇到这些词汇,简单罗列一下:

  • 一个“实体”是一个在 Core Data中定义的类。例如“员工”或“公司”。在关系数据库中,一个实体相当于一张表。
  • 一个“属性”是专门针对某个实体的一小块信息。例如,员工实体会包含员工姓名,职位和薪酬等属性。在数据库中,一个属性对应于表中的某个字段。
  • 一个“关系”是一个在多个实体间存在的连接。在 Core Data 中,位于两个实体之间的关系叫做 to-one 关系,而一个实体和多个实体之间的关系叫做 to-many 关系。例如,一个经理可能和多个员工之间存在 to-many关系,而一个员工和他的经理之间则是 to-one 关系。

注意:你也许注意到了,实体和类的概念很像。同样,属性和关系的概念和属性也很像。那又有什么区别呢?你可以把一个 Core Data 实体看成是一个类定义,把一个托管对象看成是一个类的实例。

知道了什么是“属性”之后,你可以在刚刚新建的 Person 对象中添加属性。打开 HitList.xcdatamodeld,在左手边选中 Person 实体,点击 Attributes 下边的 + 号按钮。

在 Core Data 中,一个属性拥有多种数据类型。将刚刚添加的新属性的属性名修改为 name,然后类型设置为 String:

保存到 Core Data

打开 ViewController.swift,在 import UIKit 下面导入 CoreData:

import CoreData

这条 import 语句允许你在代码中使用 Core Data API。

然后将 names 属性声明修改为:

var people: [NSManagedObject] = []

在 people 属性中,你将存入 Person 对象,而不是原来的 String,因此将数组重命名为 people,并把它作为 Table View 的数据模型。现在 people 中保存的是 NSManagedObject 实例而不是普通的字符串。

NSManagedObject 表示存储在 Core Data 中的单个对象;你只能通过它来创建、编辑和删除 Core Data 持久化存储中的数据。你稍后会看到,NSManagedObject 是一个变形怪。它可能变成任何一种只要是 Data Model 中存在的实体,只要它拥有你指定的属性和关系。

因为你修改了 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 和模型数组中的字符串进行关联,现在将 cell 和 NSManagedObject 进行关联。

注意如何从 NSManagedObject 中取出 name属性。也就是这句:

cell.textLabel?.text =
    person.value(forKeyPath: "name") as? String

为什么要这样做?因为 NSManagedObject 根本不认识什么 name属性,这个属性是你在 Data Model中定义的,因此你不能直接通过访问属性的方式来访问 name 属性。Core Data 只提供了 key-value 的方式进行访问,也就是 KVC 方式。

注意:如果你是一个新手,可能不熟悉 KVC。KVC 是一种 Foundation 中间接地通过字符串来访问对象属性的机制。换句话说,KVC 使得我们可以在运行时将 NSManagedObject 或多或少地当成字典来用。
KVC 对于任何继承于 NSObject 类的子类有效,包括 NSManagedObject。如果一个 Swift 类没有从 NSObject 继承,你就不能用 KVC 访问其属性。

然后,找到 addName 方法,将 saveAction 的实例化替换为:

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 方法进行处理。Xcode 会提示错误,因为 save方法还没有实现。在 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!上述代码解释如下:

  1. 在向 Core Data 保存或检索任何数据之前,你必须获得一个 NSManagedObjectContext对象。你可以把它看成是一个内存中的“便签”,为了使用托管对象必须用到它。

    以保存一个新的托管对象到 Core Data 为例,需要分两步进行:首先,将一个新托管对象插入到 NSManagedObjectContext,在你用这个新托管对象进行一些处理之后,通过 NSManagedObjectContext 将修改“提交”到磁盘进行保存。

    Xcode已经为我们生成了一个托管对象上下文,就在创建新项目的时候。当然,这一切只会在一开始时勾选了 Use Core Data 选项才会发生。默认情况下托管对象上下文存在于 AppDelegate的 NSPersistentContainer对象中。要访问上下文,首先要获得一份 AppDelegate 的引用。

  2. 创建了一个新的托管对象,然后将它插入到托管对象上下文。你可以将这两个动作用一步完成,即调用 NSManagedObject 的静态方法 entity(forEntityName:in:)方法。

    你可能奇怪为什么要用到 NSEntityDescription?回忆一下前面讲过,NSManagedObject 也叫做“变形怪”,因为它可以表示任何实体。而一个实体描述(NSEntityDescription)是一个在运行时将实体在数据模型中的定义和托管对象实例关联起来的连接。

  3. 拥有了 NSManagedObject 后,就可以通过 KVC 设置它的 name 属性。必须让传入的 KVC键(这里就是 name)和 Data Model 中显示的名字一模一样,否则运行时 app 会崩溃。

  4. 提交 peron 的修改并通过托管对象上下文的 save 方法保存到磁盘。注意 save 方法会抛出一个异常,所以调用它时要在 do… catch 块中使用 try 关键字。最终,将新托管对象添加到 people 数组,这样当表格刷新后新数据会显示在列表中。

这段代码比使用字符串数组时要复杂,但也没什么。这里的一些代码,比如获取托管对象上下文和实体,你只需要在 init()方法或 viewDidLoad 方法中执行一次就行了。后面可以重用这些对象。你将这些代码都放在一个方法中,只是为了简单起见。

编译运行程序,在表格中加入几个名字:

如果这些名字被存入了 Core Data,则 HitList app 应该能经过我们的考验。双击 Home 键唤起快速 app 切换程序,将 HitList 向上滑动以退出 app。

点击 Springboard中的 HitList图标,重启 app。等等,发生什么事情了?为什么列表是空的:

数据已经存到了 Core Data,但重启之后 people 数组仍然是空的!那是因为数据仍然待在磁盘上,但你还没有去读取它。

从Core Data 获取数据

要将持久存储中的数据加载进托管对象上下文,你需要先抓取数据。打开 ViewControlle.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)")
    }
}

以上代码按编号依次解释如下:

  1. 在你能够使用 Core Data 之前,需要获取托管对象上下文。抓取数据的这个部分和之前没有什么地方不同,获得 AppDelegate 引用以及通过它的持久化容器去获得一个 NSManagedObjectContext。
  2. 正如其名,NSFetchRequest 是一个用于抓取 Core Data 数据的类。这个类既足够强大又足够灵活。你可以用 NSFetchRequest 抓取符合你指定条件的多个对象(比如,给我所有住在威斯康辛州的入职满 3 年以上的公司员工),单个值(比如,给我名字最长的那个人)等等。

    NSFetchRequest 需要几个条件用于限制返回的结果集。就目前来说,你所需要知道的一个必要的条件就是 NSEntityDescription。

    设置 NSFetchRequest 的 entity 属性,或者在初始化的时候通过 init(entityName:) 指定它的 entity属性。这会返回指定实体的所有对象。在这里,你将抓取所有的 Person 实体。注意,NSFetchRequest 使用了一个泛型。通过这个泛型来指定期望它的返回类型。这里我们只是指定了这个泛型为 NSManagedObject。

  3. 将 NSFetchRequest 传递给托管对象上下文,让它完成剩下的工作。fetch 方法返回了一个符合指定条件的托管对象数组。你需要在 catch 块中捕获错误并进行必要的处理。

注意:和 save() 方法一样,fetch() 方法也会抛出异常,因此需要将它放到 do-catch块中。如果在获取数据时出现错误,你可以在 catch 块中捕获这个错误并进行相应处理。

编译运行程序,现在,你应该看到早先你所添加的人的名字了:

太好了!他们复活了(双关语)。加更多的人名到列表里,重启 app,检查保存和抓取是否正常。除了删除 app 不行以外,无论你是重置模拟器还是将手机从楼上扔下,这些人名都会在表格中显示。

结尾

你可以从这里下载完成后的项目。

在这个简单教程里,你学习了几个基本的 Core Data 概念:数据模型、实体、属性、托管对象、托管对象上下文和 Fetch Request。

如果你喜欢这个教程,你也许会对我们的这本书感兴趣:《Core Data by Tutorials》

这本书非常完整地介绍了 Core Data 框架,它是为那些已经掌握了基本的 iOS 和 Swift 开发,但想知道如何在 app中利用 Core Data 存储数据的中级 iOS 程序员准备的。

它已经完全升级到了 Swift 3、iOS10 和 Xcode 8——立即到 raywenderlich.com 商店中去看看吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值