为了完成泊学内容浏览首页,我们要从对界面内容的缓存开始。因为缓存的方式影响了对应 model 的设计,进而会影响从服务器获取结果后,对数据编码和解码的方式。这里,我们选择了使用 Core Data 作为 App 的缓存。使用它最大的好处,就是作为 Apple 钦定的本地持久化存储方案,每年至少你都可以获得一些来自官方的技术收益。并且,就它实际支持的功能来说,作为泊学的内容缓存也足够用了。

鉴于并不是每个人都熟悉 Core Data,甚至可能在 iOS 开发圈子里不熟悉它的人更多,这一节,我们用一个最简单的例子,来体验下 Core Data 用到的几个基本概念和数据存取的方法。

用到的项目和工具

为了方便接下来的研究,我们先安装一个叫做 DB Browser for SQLite 的工具,大家可以直接到官网下载安装包,或者执行 brew cask install db-browser-for-sqlite 进行安装。稍后,使用它可以帮助我们方便地察看 Core Data 数据库中的内容,因为默认情况下 Core Data 就是一个本地的 SQLite 文件:

另外一个是我为了接下来的内容说明创建的一个演示工程:CoreDataDemo,这是一个 macOS terminal 类型的项目。向大家介绍 Core Data 基本用法的代码都会放在这里:

其中:

  • main.swift 中是要演示的各种 Core Data 用法;
  • CoreDataManager.swift 中是和 Core Data 自身工作机制相关的代码;

暂时,我们只会用到这两个文件,后面有需要的时候,再创建新的。

创建 Core Data Model

接下来,为了使用 Core Data,我们要先创建一个 model 文件,也就是 Core Data 存储数据的文件。为此,在项目中,新建一个 Data Model,给它设置个名字,例如:CoreData。这样,项目中就会多出来一个叫做 CoreData.xcdatamodeld 的文件:

选择这个文件,就会在 Xcode 提供的编辑器中看到三个在 Core Data 中用到的概念:

其中,

  • ENTITIES 是 Core Data 中组织数据的单位,它通常对应着项目中的一个类。如果你熟悉数据库的话,可以把它想象成是数据库中的一张表;
  • Attributes,每个 attribute 表示 ENTITY 中存储的一类数据,我们可以把它想象成是数据库表中的一列内容;
  • Relationships 是多个 ENTITIES 之间的关系,默认情况下,是一对一的关系。当然我们也可以表达一对多,或多对多关系。如果你之前你熟悉关系型数据库,对这些概念应该也并不陌生,不过你不了解这些也没关系,稍后我们会专门提到 Relationships 的用法,现在有个概念就行了;

创建好 model 之后,我们要向其中添加一个 ENTITY。点击底部的 + 图标,给 ENTITY 起个名字。例如,我们创建了一个表示视频的 Episode。然后再点击 Attributes 底部的 + 图标,给 Episode 添加两个属性,titlesummary,分别表示视频的标题和简介。这样,model 就创建好了。

最后,为了方便稍后调试,编辑一下项目的 Run Secheme,在 Arguments 中添加一个 -com.apple.CoreData.SQLDebug 1 配置:

这样,Xcode 就会在 App 执行的时候在控制台打印和 Core Data 有关的调试信息,这有助于我们理解 Core Data 的工作方式。至此,为了操作 Core Data,所有的准备工作就都完成了。

向 Core Data 写入数据

接下来的第一个工作,就是向 Core Data 写入数据。因为如果其中没有数据,也就从无提及其它的操作了。而向 Core Data 写入数据,也是有它自己的套路的。

首先,在项目中,要 import CoreData,在接下来的内容中,我们就不再反复提及这个操作了;

其次,我们需要一个 NSManagedObjectContext 对象。这个对象可以理解成是 Core Data 数据在内存中的一份拷贝,所有对 Core Data 的操作,都是基于这份内存拷贝的。在操作完成后,我们需要另外执行一个“提交”操作,数据才会真正写入 Core Data。这和传统关系型数据库的查询操作是类似的。

在我们创建的 CoreDataDemo 项目中,CoreDataManager.swift 中已经提前为大家创建好了一个 NSManagedObjectContext 对象,用 CoreDataManager.shared.managedContext 就可以访问到它。至于它是这么来的,现在我们无需关心,只要先拿来用就行了。

第三,我们要创建一个用于描述 ENTITY 信息的类型。为此,可以使用 NSEntityDescription 中的 entity(forEntityName:in:) 静态方法:

let entity = NSEntityDescription.entity(
  forEntityName: "Episode", in: managedContext)!

它的第一个参数,表示要描述的 ENTITY 名称。第二个参数,表示操作对应的 NSManagedObjectContext 对象。这样,从代码层面,我们就有了一个指定具体是哪个 ENTITY 的对象了。

第四,根据刚才创建的 ENTITY “描述”,创建一个真正代表 ENTITY 的对象。我们将通过这个对象,从 ENTITY 中存取数据。在 Core Data 里,这种对象统一用 NSManagedObject 表示:

let episode = NSManagedObject(entity: entity, insertInto: managedContext)

它的第一个参数是上一步创建的表示 ENTITY 的对象,第二个参数是 ENTITY 对象所在的 NSManagedObjectContext 对象;

第五,有了 NSManagedObject 之后,就可以向 managedContext 写入数据了。由于 NSManagedObject 抹掉了要写入的具体数据类型,这个过程只能通过 KVC 的方式完成。于是,为了写入 titlesummary,我们只能这样:

episode.setValue("Core data demo I", forKeyPath: "title")
episode.setValue("Core data basic ideas.", forKeyPath: "summary")

第六,等所有写入操作完成后,为了把它们真正保存到 Core Data,我们要把 managedContext 中的数据,写回 Core Data:

CoreDataManager.shared.saveContext(managedContext)

同样,至于 saveContext 的具体实现,我们先不用管,只要知道逻辑上它完成的工作就好了。至此,数据就应该顺利写入到 Core Data 了。直接执行下,在 Xcode 控制台就会看到类似下面这样的信息:

CoreData: annotation: Connecting to sqlite database file at "/Users/mars/Library/Application Support/CoreDataDemo/CoreData.sqlite"

这里,打印了本地 Core Data 数据库的文件路径,用之前安装过的 DB Browser for SQLite 打开这个文件,就会看到类似这样的结果:

也就是说,数据已经写入到 Core Data 了。

从 Core Data 读取数据

接下来,我们再来看如何从 Core Data 中读取数据,它也有自己固定的套路。首先,创建一个表达“从特定 ENTITY 读取数据请求”的 NSFetchRequest 对象:

let fetchRequest =
  NSFetchRequest<NSManagedObject>(entityName: "Episode")

它的参数,就是要读取的 ENTITY 的名字。其次,是让之前创建的 managedContext 利用 fetchRequest 读取数据:

let episodes = try! managedContext.fetch(fetchRequest)

同样,我们忽略了这个操作可能抛出的错误。这时得到的 episodes,是一个 [NSManagedObject],表示 Episode ENTITY 中的所有记录,和写入时类似,这个结果也屏蔽掉了具体的类型信息,为了从中读取每一个记录的 titlesummary,我们也要借助于 KVC:

episodes.forEach {
  let title = $0.value(forKeyPath: "title")!
  let summary = $0.value(forKeyPath: "summary")!

  print("\(title): \(summary)")
}

重新执行下,在控制台就可以看到打印的记录信息了。不过这次,我们应该可以看到打印了两条记录。这是因为,我们一共执行了两次程序,每次都会向 core data 的 Episode entity 写入一条相同的记录。

通常,写入相同的记录,并不是我们期望的行为。但在了解如何正式处理这类问题之前,一个更简单的办法就是每次执行 App 之前,通过 Xcode 提示的路径把上一次的 sqlite 文件删掉。这样,每次执行就不会带有之前的历史数据了。在接下来的内容中,我们也会采用这个方法。

What's next?

至此,关于 Core Data 中最基本的概念和操作,我们就说完了。除了一开始提到的 NSManagedObjectContext 之外,其余的部分还是很好理解的。另外,你可能会觉得,存取数据是没有类型信息,只能依赖于 KVC 并不是太好用,也不安全。没错,在下一节里,我们就来解决这个问题,用更加 Swift 的方式通过 Core Data 读取数据。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们