Core data swift

在 Swift 中使用 Core Data

Core Data 是一个非常强大的框架,但是它的使用难度也不小。尽管如此,它仍是广大 iOS 开发者的不二选择。Swift 同样也是一门强大的编程语言,并且它还十分简单易学。那么,如果将两者结合起来,借助 Swift 控制 Core Data,我们能否减少 Core Data 的使用难度呢?

在本次演讲中,Jesse Squires 将会为我们讲述他在使用 Swift 操控 Core Data 中遇到的心得。此外,他还为我们分享了诸多策略,包括如何让模型摆脱 Objective-C 风格的影响,您可能会遇到的困难和错误,如何解决这些问题,以及 Swift 是如何让模型对象的概念更为清晰的。我们同时还可以学到如何在 NSManagedObject 子类中驾驭 Swift 的特定功能,比如说枚举���

要查看本次演讲中使用的代码,您可以查看Jesse 的 GitHub


何谓 Core Data? (0:10)

Core Data 框架提供了一种通用、自动化的解决方案,包括对对象进行生命周期、层级管理,以及持久化存储等一系列常见操作。

  • Core Data 编程指南

Core Data 是一个用以管理对象生命周期、对象层级以及进行持久化存储的苹果官方框架。它的底层由 SQLite 完成,但是它并不是一个关系型数据库。因此根据你的实际情况来决定是否使用 Core Data,因为它不一定是最佳的选择。Core Data 的优点在于在同步过程中,它能始终维持你对象之间的关系。

Core Data 栈 (3:12)

在 Core Data 栈(stack)框架的最上层,我们能够对管理对象上下文(managed object context)进行操作,用来创建对象。这和 NSManagedObject 非常相似,但是对于 Core Data 而言,它们拥有更多的表现和属性。创建对象之后,上下文将拥有对象的实例,这样你就可以在上下文中操作或者修改对象、创建关系,或者进行存储。下面一层是存储区域协调层(store coordinator),用来管理不同的数据存储区域。在大多数情况下,在 SQLite 背后你往往只会使用一个存储区域,不过也有例外。其中一个例子就是内存存储区(in-memory store),你可以在其中使用 Core Data,而无需担心数据持久化的问题。再下面一层则是持久存储层(persistent store),由一个 NSPersistentStore 对象来表示。这是你硬盘上实际的存储区域,就像 UIImage 包含了存储在硬盘上的图像,NSBundle 包含了存储在硬盘上的应用程序资源。大部分情况下,在 Core Data 栈建立起来之后我们就无需关心其他部分,只需处理上下文和管理对象即可。

为什么要用 Core Data?为什么要用 Swift? (5:26)

Core Data 能够提供你所需的绝大多数功能。正如苹果所说,它是“成熟的、单元测试过并且性能最优的”。同样,它也是 iOS 和 OS X 工具链的一部分,因此它是内置在这两个平台当中的。使用 Core Data 是减少第三方库依赖的绝佳方式,因为它已经集成在 Xcode 上面了。苹果同样也对其加大了投资,在去年的 WWDC 上围绕 Core Data 的新特性已经有过不少的演讲,因此这个框架将会被苹果长期支持更新。它也十分受开发者欢迎,在线上拥有不少的资源。

然而,这些资料通常情况下都是由 Objective-C 写的,因此为什么我们要使用 Swift 呢? Swift 能够给你的代码带来极大的简洁性,从而在某些程度上能够更简单地使用 Core Data。我们能够使用类型安全以及某些 Swift 专有的特性,比如说枚举、可选值等等。此外,我们还可以在 Swift 中使用函数式编程,而这在 Objective-C 中是不能够完成的。

Swift + Core Data (8:08)

使用 Swift 之前,你需要注意:这货仍然还未成熟。通常情况下,Core Data 和 Swift 仍然还不是非常稳定,虽然它们始终在进行改进,但是它也会让人非常恼火。

Receive news and updates from Realm straight to your inbox

Core Data 栈 (8:43)

我们首先所需要做的就是搭建 Core Data 栈。在前面的图中,我们想将所有不同的组件封装到某些对象当中,以便重用。这和我们在 Objective-C 中搭建样本代码的操作十分类似,但是过程却十分愉快。我们从我们的 Core Data 模型开始,这是一个值类型的结构体,拥有一个名字属性和bundle属性,以及其他能够更好处理我们模型的属性和方法。

struct CoreDataModel {
  let name: String
  let bundle: NSBundle
  init(name: String, bundle: NSBundle)
  // 其余属性和方法...
}

之后就是创建栈,在初始化这个栈的时候它将接收我们的模型。初始化构造器会自行设置这三个属性。对于存储类型的数据来说,我们就能够提供默认值,比如二进制数据存储、SQLite 存储以及内存存储。我们还可以并发执行,然后指定主线程作为默认类型。接下来,如果我们想要进行单元测试或者创建另外的栈,我们就可以初始化不同的存储区或者专有队列了。

let model = CoreDataModel(name: "MyModel", bundle: myBundle)
let stack = CoreDataStack(model: model,
                      storeType: NSSQLiteStoreType,
                concurrencyType: .MainQueueConcurrencyType)

// 使用上下文
stack.managedObjectContext

不要把它们放到 App Delegate 当中! Xcode 会自行在 App Delegate 中设置了所有的基础操作,并且这些操作不会在你应用的其余部分进行重用。如果你执意这样做的话,会创建出你不想要的依赖,然后代码就会锁死在 App Delegate 当中,而这则是应该避免的。沿着这一思路,我们可以使用框架来进行。随着 iOS 8 特别是 Swift 的发布,我们现在可以创建 Cocoa Touch 框架,而不是像往常那样使用静态库。

创建管理对象 (15:27)

Xcode 会自行为你生成类,这再好不过了。不过在 Swift 当中,这却是一个非常糟糕的做法。mogenerator 是一个很多人在 Objective-C 中使用的第三方库,但是它的 Swift 的版本还处于实验阶段,并且上一次在 GitHub 上的更新是在14年9月。所以,这就是我们所说的工具问题,由于某些工具还不成熟,因此我们不能够使用它们。

当你创建对象的时候,你将得到这个可视化编辑器,因此你可以在其中创建它们的属性和关系。我们同样可以为数据建立属性验证(attribute validation)、自定义规则以及约束。我们可以设定键值是否可选,提供最大最小值,当你保存数据的时候,这些东西都将自行检查。

下面的代码比较了 Xcode 自行生成的正常 Objective-C 管理对象子类和 Swift 子类的区别。在 Objective-C 中,我们拥有一个 employee 对象,并且所有的属性都能得到支持。但是我们无法告诉 Core Data,在这个模型文件中哪些值是可空的,也无法设置任何默认值。感谢 Swift 的简洁语法,其对应的 Swift 子类则变得十分清晰。可选值允许空值,只需要加一个问号标注即可。Swift 保证任何未被标为可选值的数据是不可能为空的。Swift 中的 @NSManaged 拥有和 Objective-C 中的 @dynamic 拥有相似的功能。这些属性的存储和实现将在运行时提供给编译器。

// Objective-C
@interface Employee: NSManagedObject

@property (nonatomic, retain) NSString *address;
@property (nonatomic, retain) NSDate *dateOfBirth;
@property (nonatomic, retain) NSString *email;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSDecimalNumber *salary;
@property (nonatomic, retain) NSNumber *status;

@end
// Swift
class Employee: NSManagedObject {
  @NSManaged var address: String?
  @NSManaged var dateOfBirth: NSDate
  @NSManaged var email: String?
  @NSManaged var name: String
  @NSManaged var salary: NSDecimalNumber
  @NSManaged var status: Int32
}

另一件好事就是 Swift 类拥有命名空间,也就是它们所在的模块。这意味着我们需要在我们的类名称前面加上模块名。Xcode 并不会为你做这件事,因此我们需要在建立类之后手动添加。如果不使用前缀将会导致运行崩溃以及其他未预料的错误。

实例化管理对象 (22:07)

现在我们想要在代码中使用这些对象了。对于初始化管理对象已经有一些样本代码了,因此我们只需要使用 Core Data 中描述实体(entity)的 NSEntityDescription 类即可。这些实体描述是可以将对象插入到 Core Data 中的方法,尽管它是一个非常臃肿难看的方法。要是实际获取我们类的实例的话,我们需要写一个辅助方法,来获取实例名称并将其放置到上下文当中。

// "Person"
NSString *name = [Person entityName];

@implementation NSManagedObject (Helpers)

+ (NSString *)entityName
{
  return NSStringFromClass([self class]);
}
@end

// 创建新的对象
Person *person = [Person insertNewObjectInContext:context];

@implementation NSManagedObject (Helpers)
+ (instancetype)insertNewObjectInContext:(NSManagedObjectContext *)context
{
  return [NSEntityDescription insertNewObjectForEntityForName:[self entityName]
                                       inManagedObjectContext:context];
}

@end

对于 Swift 来说,我们需要使用 Objective-C 运行时的 getClass 函数,它将会返回 MyApp.Person 的全部合适名称。但是在 Core Data 中,你的对象仅仅只是叫 Person 罢了,因此我们必须做一些分析。我们通过名称创建一个“实体描述”,然后将其放到上下文中,再调用 NSManagedObject 的构造器。

// "MyApp.Person"
let fullName = NSStringFromClass(object_getClass(self))
extension NSManagedObject {
  class func entityName() -> String {
    let fullClassName = NSStringFromClass(object_getClass(self))
    let nameComponents = split(fullClassName) { $0 == "." }
    return last(nameComponents)!
  }
}
// "Person"
let entityName = Person.entityName()

// 创建新的对象
let person = Person(context: context)
extension NSManagedObject {
  convenience init(context: NSManagedObjectContext) {
    let name = self.dynamicType.entityName()
    let entity = NSEntityDescription.entityForName(name,     inManagedObjectContext: context)!
    self.init(entity: entity,     insertIntoManagedObjectContext: context)
  }
}

// 指定的构造器
class Employee: NSManagedObject {
  init(context: NSManagedObjectContext) {
    let entity = NSEntityDescription.entityForName("Employee",
    inManagedObjectContext: context)!
    super.init(entity: entity,
    insertIntoManagedObjectContext: context)
  }
}

这种方法的缺点是,我们不需要为所有的类执行这样的操作。这就不符合 Swift 简洁明了的特性了,我们刚刚所做的,无非只是用新的语法完成了 Objective-C 所做的罢了,这不符合我们的目的。我们想要使用 Swift 的特性,并且以 Swift 的方法来使用 Core Data。Objective-C 的方法不适合 Swift 的选择!

真正的“Swift”风格

初始化 (27:53)

首先,我们所需要做的就是为这些对象创建一个实际的指定构造器。所有的属性都需要赋予一个初值,并且指定的构造器必须要完全初始化它们。其次是便利构造器,他需要调用上面的这个指定构造器。子类中通常情况下并不会继承父类的构造器。接下来,对于NSManagedObject 来说,我们需要对其添加实际的指定构造器。

// 指定构造方法
init(entity:insertIntoManagedObjectContext:)

// 便利构造方法
convenience init(context:)

然而,由于 @NSManaged 的存在,Core Data 会跳过构造器规则。Swift 并不知道你应该如何处理这些属性,由于在运行时它们他能够提供给编译器,因此它们并没有被初始化。相反,我们可以添加一个实际的构造器,然后提取所有的参数再进行依赖注入(dependency injection),这样我们就可以给这些参数提供初始值了。

class Employee: NSManagedObject {
  init(context: NSManagedObjectContext,
          name: String,
   dateOfBirth: NSDate,
        salary: NSDecimalNumber,
    employeeId: String = NSUUID().UUIDString,
         email: String? = nil,
       address: String? = nil) {
        // 初始化
       }
}

别名 (30:52)

第一个我们可以使用的 Swift 特性就是typealias了。这十分简单,并且并不仅仅只能在 Core Data 中使用,你可以在应用的其他任何地方使用这个特性。在这个例子中,我们将字符串建立一个 EmployeeId 的别名,这样我们就可以在代码中更好地理解 EmployeeId 的实际意义,而不是纠结于 String 的实际意义。通过别名,我们能够让代码更加清晰移动,这个别名就和实际类型一样,没有任何区别。

typealias EmployeeId = String
class Employee: NSManagedObject {
  @NSManaged var employeeId: EmployeeId
}

// 例子
let id: EmployeeId = "12345"

关系 (31:40)

在 Swift 1.2 中,我们有了内建的集合类型。这个集合类型是值类型的,建有泛型,比起 NSSet 或者其他任何集合对象来说更加好用。当你在对象之间建立关系的时候,比如说一对多关系,你可以让一个集合中存放指向其他对象的关系,这样就可以保证关系不重复。不过很不幸的是,Set 并不和 Core Data 兼容,它并不知道如何处理这个类型。

枚举 (32:25)

在我们的管理对象中我们可以使用枚举。比如说,我们可以拥有一个名为 Genre 的枚举,用来存放乐队和唱片。我们的 NSManagedValue 可以设置为私有,属性则是公开的。

public class Band: NSManagedObject {
  @NSManaged private var genreValue: String
  public var genre: Genre {
    get {
      return Genre(rawValue: self.genreValue)!
    }
    set {
      self.genreValue = newValue.rawValue
    }
  }
}

// 原来的代码
band.genre = "Black Metal"
// 新的代码
band.genre = .BlackMetal

然而,Core Data 并不知道如何处理枚举,因此我们需要使用私有的属性来获取这个请求。在这个例子中获取请求的时候,我们仍必须使用键值对来获取值的名称。在这些框架中,我们能够切实感受到 Objective-C 带来的包袱。

let fetch = NSFetchRequest(entityName: "Band")
fetch.predicate = NSPredicate(format: "genreValue == %@", genre)

函数式编程 (34:27)

最后一个特性就是 Swift 的函数式编程。Chris Eidhof 在 tiny networking 上已经有所介绍,如何通过简单的代码完成极其复杂的事务。

存储 (34:42)

要存储上下文,我们可以使用一个存储方法和一个返回布尔值的错误指针。如果要让其更 Swift 风格化,我们创建一个名为 save 的全局函数来接受上下文。我们封装好这个防范,然后返回带有布尔值和错误的元组,然后执行存储,检查存储是否成功。这样,你就无需处理这些错误指针了。

var error: NSError?
let success: Bool = managedObjectContext.save(&error)
// 处理成功或者失败

// 让其更 Swift 风格化
func saveContext(context:) -> (success: Bool, error: NSError?)

// 例子
let result = saveContext(context)
if !result.success {
  println("Error: \(result.error)")
}

提取请求 (35:23)

对于提取请求(fetch requests)来说,现有的方法就是在上下文的某个方法中执行提取请求。同样,我们在里面看到了错误指针,由于它是由 Cocoa 直接桥接而来,因此它通过AnyObject类型的可选数组来取得结果。这意味着我们必须将其转换为实际的对象。

var error: NSError?
var results = context.executeFetchRequest(request, error: &error)
// [AnyObject]?
if results == nil {
  println("Error = \(error)")
}

要改进这一点,我们可以建立一个提取请求的子类,增加泛型参数。接下来我们就可以使用这个接收 T 类型的 FetchRequest 的全局泛型函数了。

// T 是泛型
class FetchRequest <T: NSManagedObject>: NSFetchRequest {
  init(entity: NSEntityDescription) {
    super.init()
    self.entity = entity
  }
}

typealias FetchResult = (success: Bool, objects: [T], error: NSError?)
func fetch <T> (request: FetchRequest<T>,
                context: NSManagedObjectContext) -> FetchResult {
    var error: NSError?
    if let results = context.executeFetchRequest(request, error: &error) {
      return (true, results as! [T], error)
    }
      return (false, [], error)
}

在运行过程中,代码会像这个样子。回到我们的逻辑代码中来,我们为一个约定创建了一个请求。我们可以提取这个请求,获取结果,然后使用结果。我们获取到的对象拥有指定的类型,并且我们还得到了相应对象的数组。

// 例子
let request = FetchRequest<Band>(entity: entityDescription)
let results = fetch(request: request, inContext: context)

if !results.success {
  println("Error = \(results.error)")
}

results.objects // [Band]

如果你怀念 Objective-C 优点的话,那么我们在 Swift 中确实已经丧失了许多。诸如 Mantle 之类的第三方优秀库可以极大地简化 Core Data 使用和存储的操作,但是它极大地依赖于 Objective-C 运行时和其的动态特性。

综述以及问答 (40:29)

回顾一下,我们的目标之一是让代码更加简洁明了。我们通过可选值、枚举、别名和指定构造器来完成这个目标。通过指定构造器可以清楚地为我们的模型指定默认值以及可选值。我们不仅只是创建了一个无需初始化其所有属性的对象。我们利用了诸如枚举、可选值之类的 Swift 特性,并且我们将这些特性应用到了存储和查询操作当中来。实际上,我们可以做的还有很多。

要查看我对 Core Data 的 Swift 封装框架的话,可以在这里查看我的 GitHub。

问:你在实际产品中使用 Swift 的感受如何? Jesse:网上关于这方面的文章很多,但是我至今还没有在实际产品中使用 Swift。我觉得苹果更新这门语言的速度很快,并且在实际产品中使用 Swift 的话会带来很多优点。或许使用 Swift 最大的缺点就是第三方库的缺乏了,而这些三方库往往可以给开发带来极大地便利。就本次演讲而言,我认为 Swift 比起 Objective-C 要好用很多,尤其是指定构造器的使用。

问:这些新技术是否适用于现有基于 Core Data 撰写的应用呢?有必要将所有代码替换为 Swift 吗? Jesse:这些新特性并不足以让你替换所有代码,这不是一个充分的理由。替换代码得取决于你的实际情况,并且也取决于你是否能从这些新特性中获取到好处。因为从 Objective-C 代码迁移到 Swift 代码要花费极大的精力和时间。还有一点是我们所介绍的这些新特性并不能和 Objective-C 很好地兼容。因此,如果你只是简单的重写了数据模型,你可能会在代码的其余部分遇见问题,你会发现你根本无法访问这些东西!因此你就得不停地修改,以让你的代码能够适应它。


About the content

This content has been published here with the express permission of the author.

Jesse Squires

Jesse is an iOS developer at Instagram who writes about Swift and Objective‑C on his blog at jessesquires.com. He’s the curator of the Swift Weekly Brief newsletter and co-host of the Swift Unwrapped podcast. He is fueled primarily by black coffee and black metal.

4 design patterns for a RESTless mobile integration »

close