iOS Apprentice中文版-从0开始学iOS开发-第四十一课

使用Core Data存储位置信息

此刻你已经拥有了一个app,可以获得用户当前的GPS坐标。它还有一个界面可以用来标记位置信息,可以添加一段描述并且选择一个分类。稍后,你将添加照片功能进去。

接下来我们要把用户的位置信息保存到一个列表里去。

这个界面完成后会是下面这个样子:

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第1张图片

你需要用某种方法把获取到的位置信息保存下来。

上一次你保存信息的时候,你创建了符合NSCoding协议的数据模型对象,并且使用NSKeyedArchiver将它们保存到.plist文件中。这样确实可以达到很好的效果,但是这节课,我会给你介绍一个新的框架,它可以帮你节省很多时间,那就是Core Data。

Core Data是用于iOS应用的一个持久化存储对象。如果你之前自己看过关于Core Data的文档的话,你也许发现它很难,但是实际上它的原理非常简单。

你之前学到过假如一个对象没有其他对象引用它的时候,这个对象会被销毁掉。此外,所有对象都会在app被关闭后销毁掉。

通过使用Core Data,你可以指定一些对象持久化存储,它们会被保存到数据商店中。这样即使所有引用它们的对象以及实例都被销毁了,它的数据还是安全的存储在Core Data中,并且你可以随时把它取回来。

如果你之前学习过数据库的话,你会发现它们很像,只是Core Data保存的对象,而数据库保存的是关系表。

添加Core Data到app中。

Core Data需要使用数据模型。这是一个特殊的文件用于表述你要持久存储的对象。与常规对象不同,这些托管对象会将数据保存起来,除非你本人要求删除它们。

添加一个新的文件到工程中,在文件模版处选择Core Data分节下的Data Model文件类型:

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第2张图片

将文件命名为DataModel。

此时你可以在工程导航器中看到一个DataModel.xcdatamodeld文件。

选定这个文件,就可以打开数据模型编辑器了:

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第3张图片

你用Core Data管理的每一个对象,都要先为它创建一个实体(entity)。

一个实体描述你的对象将拥有哪些数据字段。 从某种意义上来说,它的作用与一个类相同,但是是专门用于Core Data的数据存储。(如果你以前学习过数据库的话,你可以把一个实体当作一个表)

我们的这个app仅有一个实体,就是位置信息,每个位置信息包含以下数据:

1、经纬度

2、街道信息

3、时间

4、用户添加的描述

5、分类

这些都是Tag Location界面上的内容,照片除外。 照片可能会非常大,它可能需要几兆字节的存储空间。 虽然Core Data存储可以处理大量的“blobs(不知道啥意思)”数据,但是最好将照片作为单独的文件存储在应用程序的Documents目录中,后续我们会单独讲解关于照片存储的内容。

点击数据模型编辑器底部的Add Entity按钮。这样就会在ENTITIES标签下新增一个实体。将其命名为Location。在右边的面板中单击一下就可以重命名了,见下图蓝色被选定的那个记录:

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第4张图片

在实体的内部有三个分节,Attributes,Relationships以及Fetched Properties。Attributes就是实体的数据字段。

这个app仅有一个实体,但是通常app会有多个实体并且相互关联。通过Relationships与Fetched Properties你可以告诉Core Data你的对象间的依赖关系。

对于我们这个app而言,只需要使用Attributes。

点击编辑器底部的Add Attribute按钮,或者Attributes分节下的小加号按钮添加新的属性。给它命名为latitude,并且将类型设置为Double:

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第5张图片

Attributes大体上盒实例变量很像,因此它也有类型的概念。之前我们介绍过latitude和longitude都是Double型的,所以它们对应的Attributes也应该是Double型的。

⚠️:不要让这些术语把你吓着,你可以这样想:
entity = object or class
attribute = variable
如果你想知道方法在Core Data中对应什么,我可以告诉你不存在这种概念。Core Data仅用于存储对象的数据部分。实体的概念是这样的:对象的数据,对象间的关系(如果存在的话)。
稍后,你将创建一个Swift文件来定义自己的Location类。 它将与数据模型中的Location实体关联。 但它仍然是一个普通的类,所以你可以添加你自己的方法。

添加剩余的attributes到Location实体中:

longitude,类型Double

date,类型Date

locationDescription,类型String

category,类型String

placemark,类型Transformable

现在数据模型看起来应该是这个样子:

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第6张图片

这里提一下locationDescription,你不能将这个字段命名为description,因为description是NSObject中的一个对象。如果你这样做了的话,Xcode会毫不客气的给出一个报错。

placemark的类型是Transformable。Core Data仅支持比较有限的类型,比如String、Double、Date。但是placemark的类型是CLPlacemark,Core Data并不能直接支持这种类型。

幸运的是,Core Data有一种处理任意数据的规则。任何遵循NSCoding协议的类可以被存储为Transformable类型。更加幸运的是CLPlacemark正好遵循NSCoding协议,所以你可以直接存储它,不用做什么额外的工作。

默认情况,实体的属性都是可选型,意味着它们可以为nil。在我们的app中只有placemark可能为nil,如果地址解析失败的话。所以我们可以在Xcode中指明这一点(其实不做也无所谓)。

选择category属性。在属性面板中,取消选中Optional选项,见下图:

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第7张图片

然后用同样方法,把除了placemark以外的所有属性的Optional选项都取消选中。

然后使用command+S保存一下,虽然Xcode支持自动保存,但是我们最好还是养成自己随时保存的习惯。

数据模型基本处理完了,但是还有一件事我要特别讲一下。

点击选定Location实体,然后打开属性面板。

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第8张图片

你可以看到Class这个字段中填写的是“NSManagedObject”。当你从Core Data中取回这个实体时,你会得到一个NSManagedObject类的对象。

这是Core Data管理的所有的对象的基础类。常规对象从NSObject继承,但来自Core Data的对象是由NSManagedObject扩展来的。

因为直接使用NSManagedObject是有点限制的,所以你可以使用自己的类来代替。 虽然并不是必须要这样做,但是这样确实会使Core Data的使用简单一些。

现在我们要做的是,从数据存储中取回Location实体时,直接返回一个Location的实例,而不是NSManagedObject。

首先在属性面板中将Codegen这一栏中的内容选择为Manual/None。

⚠️:从版本8开始,Xcode可以从数据模型自动生成实体类的源代码。 Codegen设置决定了它如何做到这一点。 为了本教程的目的,我们暂时不使用自动代码生成,这就是你将Codegen设置为Manual / None的原因。 了解如何制作自己的NSManagedObject子类,而不是完全依靠Xcode是很有用的。

即使你不使用自动类生成,Xcode仍然可以提供帮助。

选择菜单Editor → Create NSManagedObject Subclass。

然后会弹出一个窗口让你选择实体和数据模型。

选择DataModel,然后点击Next。然后选择Location再点击Next。

iOS Apprentice中文版-从0开始学iOS开发-第四十一课_第9张图片

然后选择保存位置,点击Create结束。

现在两个新的文件被添加到工程中了。第一个是Location+CoreDataClass.swift,它里面的内容是这样的:

import Foundation
import CoreData
public class Location: NSManagedObject {
}

和你看到的一样,Location继承了NSManagedObject,而不是新建了一个NSObject。

第二个文件是Location+CoreDataProperties.swift:

import Foundation
import CoreData


extension Location {

    @nonobjc public class func fetchRequest() -> NSFetchRequest {
        return NSFetchRequest(entityName: "Location")
    }

    @NSManaged public var latitude: Double
    @NSManaged public var longitude: Double
    @NSManaged public var date: NSDate?
    @NSManaged public var locationDescription: String?
    @NSManaged public var category: String?
    @NSManaged public var placemark: NSObject?

}

在这个文件中,Xcode创建了与实体中的属性相对应的实例。这里你没有见过的东西是extension关键字。

通过extension(扩展),你可以将其他功能添加到现有对象,而无需更改该对象的源代码。 这甚至适用于你实际上没有这些对象的源代码。 稍后在本教程中,您将看到一个如何使用扩展向iOS框架中的对象添加新方法的示例。

在这里,扩展名被用于其他目的。 如果稍后更改了Core Data模型,并且想要自动更新代码以匹配这些更改,则可以再次选择“创建NSManagedObject子类”,而Xcode将只覆盖“Location + CoreDataProperties.swift”中的内容,但不会添加任何内容到Location + CoreDataClass.swift。

因此,如果你打算在之后覆盖此文件,则更改Location + CoreDataProperties.swift并不是一个好主意。 不幸的是,Xcode在属性的类型上有所不满,所以你不得不对这个文件做一些修改。

首先要解决的是placemark变量。 因为你创建了一个Transformable类型的placemark,所以Xcode并不知道这是什么类型的对象,所以它选择了泛型类型NSObject。但是你知道这将是一个CLPlacemark对象,所以你可以做些改变来让事情变得更容易。

首先在Location+CoreDataProperties.swift中导入CoreLocation框架:

import CoreLocation

然后将placemark属性修改为:

@NSManaged var placemark: CLPlacemark?

问号不能去掉,因为placemark是个可选型。

同时将date属性的类型修改为Date:

@NSManaged var date: Date

NSDate是OC中的用法,在Swift中,我们使用Date。并且去掉问号,它不再是一个可选型。

最后,删除category和locationDescription属性后面的问号。 之前你告诉Core Data这些属性不是可选项,所以他们不需要这个问号。

由于这是一个托管对象,并且数据存在于数据存储区内,因此Swift将以特殊方式处理Location的变量。 @NSManaged关键字告诉编译器这些属性将在运行时由Core Data解析。 当你为这些属性添加一个新的值的时候,Core Data会把这个值放到数据存储中以保存,而不是放在一个普通的实例变量中。

这样就结束了这个app的数据模型的定义。 现在你必须把它连接到一个数据存储。

数据存储

在iOS系统中,Core Data将其所有数据存储到SQLite数据库(发音为“SQL light”)。 如果你不知道SQLite是什么,那也行。 稍后你将看到该数据库,但是你并不需要知道数据存储中为了使用Core Data而做了些了什么。

但是,当应用程序启动时,你必须初始化这些数据存储。 对于使用Core Data的app来说,这个代码是相同的,并且它在app的委托类中。

app委托(app delegate)是获取与app有关的通知的对象。 例如,这是iOS通知应用程序启动的地方。

你会在AppDelegate中做些修改。

打开AppDelegate.swift并且导入Core Data框架(在顶部第一行):

import CoreData

在AppDelegate类的内部添加以下代码:

lazy var persistentContainer: NSPersistentContainer = {
  let container = NSPersistentContainer(name: "DataModel")
  container.loadPersistentStores(completionHandler: {
    storeDescription, error in
    if let error = error {
      fatalError("Could load data store: \(error)")
    }
})
  return container
}()

这是就是你需要加载之前定义的数据模型的代码,并将其连接到SQLite数据存储。

这里的目标是创建一个所谓的NSManagedObjectContext对象。 这是你将用于与CoreData交谈的对象。 为了获得NSManagedObjectContext对象,应用程序需要做几件事情:

1、从你之前创建的CoreDatamodel中创建一个NSManagedObjectModel。 该对象表示运行时的数据模型。 你可以从其中得到类型的实体,这些实体有什么属性,等等。 在大多数app中,你不需要直接使用NSManagedObjectModel对象。

2、创建一个NSPersistentStoreCoordinator对象。该对象负责SQLite数据库。

3、最后,创建NSManagedObjectContext对象并将其连接到持久存储协调器。

这些对象一起被称为“Core Data stack核心数据栈”。

在iOS 9中,你必须手动执行这些步骤,这可能会有点混乱。 幸运的是,iOS 10中有一个新的对象NSPersistentContainer,它负责处理所有事情。

这并不意味着你应该立即忘记你刚才了解到的NSManagedObjectModel和NSPersistentStoreCoordinator的内容,但是它可以帮你避免编写一堆代码。

刚刚添加的代码将创建一个类型为NSPersistentContainer的实例变量persistentContainer。 为了得到我们之后的NSManagedObjectContext,你可以简单的访问一下persistentContainer的viewContext属性。

为了方便起见,添加一个新属性来从持久容器中获取NSManagedObjectContext:

 lazy var managedObjectContext: NSManagedObjectContext = self.persistentContainer.viewContext

现在,我们已经做好使用CoreData的准备工作了。

编译一下app确保没有错误。 如果你运行app,你不会发现任何区别,因为你实际上还没有使用Core Data。

你可能感兴趣的:(iOS Apprentice中文版-从0开始学iOS开发-第四十一课)