内容简介:在 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 框架提供了一种通用、自动化的解决方案,包括对对象进行生命周期、层级管理,以及持久化存储等一系列常见操作。
- Core Data 编程指南
Core Data 是一个用以管理对象生命周期、对象层级以及进行持久化存储的苹果官方框架。它的底层由 SQLite 完成,但是它并不是一个关系型数据库。因此根据你的实际情况来决定是否使用 Core Data,因为它不一定是最佳的选择。Core Data 的优点在于在同步过程中,它能始终维持你对象之间的关系。
在 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?
Core Data 能够提供你所需的绝大多数功能。正如苹果所说,它是“成熟的、单元测试过并且性能最优的”。同样,它也是 iOS 和 OS X 工具链的一部分,因此它是内置在这两个平台当中的。使用 Core Data 是减少第三方库依赖的绝佳方式,因为它已经集成在 Xcode 上面了。苹果同样也对其加大了投资,在去年的 WWDC 上围绕 Core Data 的新特性已经有过不少的演讲,因此这个框架将会被苹果长期支持更新。它也十分受开发者欢迎,在线上拥有不少的资源。
然而,这些资料通常情况下都是由 Objective-C 写的,因此为什么我们要使用 Swift 呢? Swift 能够给你的代码带来极大的简洁性,从而在某些程度上能够更简单地使用 Core Data。我们能够使用类型安全以及某些 Swift 专有的特性,比如说枚举、可选值等等。此外,我们还可以在 Swift 中使用函数式编程,而这在 Objective-C 中是不能够完成的。
Get more development news like this
使用 Swift 之前,你需要注意:这货仍然还未成熟。通常情况下,Core Data 和 Swift 仍然还不是非常稳定,虽然它们始终在进行改进,但是它也会让人非常恼火。
我们首先所需要做的就是搭建 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 框架,而不是像往常那样使用静态库。
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 并不会为你做这件事,因此我们需要在建立类之后手动添加。如果不使用前缀将会导致运行崩溃以及其他未预料的错误。
现在我们想要在代码中使用这些对象了。对于初始化管理对象已经有一些样本代码了,因此我们只需要使用 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”风格
首先,我们所需要做的就是为这些对象创建一个实际的指定构造器。所有的属性都需要赋予一个初值,并且指定的构造器必须要完全 初始化
它们。其次是便利构造器,他需要调用上面的这个指定构造器。子类中通常情况下并不会继承父类的构造器。接下来,对于 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) { // 初始化 } }
第一个我们可以使用的 Swift 特性就是 typealias
了。这十分简单,并且并不仅仅只能在 Core Data 中使用,你可以在应用的其他任何地方使用这个特性。在这个例子中,我们将字符串建立一个 EmployeeId
的别名,这样我们就可以在代码中更好地理解 EmployeeId
的实际意义,而不是纠结于 String 的实际意义。通过别名,我们能够让代码更加清晰移动,这个别名就和实际类型一样,没有任何区别。
typealias EmployeeId = String class Employee: NSManagedObject { @NSManaged var employeeId: EmployeeId } // 例子 let id: EmployeeId = "12345"
在 Swift 1.2 中,我们有了内建的集合类型。这个集合类型是值类型的,建有泛型,比起 NSSet
或者其他任何集合对象来说更加好用。当你在对象之间建立关系的时候,比如说一对多关系,你可以让一个集合中存放指向其他对象的关系,这样就可以保证关系不重复。不过很不幸的是, Set
并不和 Core Data 兼容,它并不知道如何处理这个类型。
在我们的管理对象中我们可以使用枚举。比如说,我们可以拥有一个名为 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)
最后一个特性就是 Swift 的函数式编程。Chris Eidhof 在tiny networking 上已经有所介绍,如何通过简单的代码完成极其复杂的事务。
要存储上下文,我们可以使用一个存储方法和一个返回布尔值的错误指针。如果要让其更 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)") }
对于提取请求(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 运行时和其的动态特性。
回顾一下,我们的目标之一是让代码更加简洁明了。我们通过可选值、枚举、别名和指定构造器来完成这个目标。通过指定构造器可以清楚地为我们的模型指定默认值以及可选值。我们不仅只是创建了一个无需初始化其所有属性的对象。我们利用了诸如枚举、可选值之类的 Swift 特性,并且我们将这些特性应用到了存储和查询操作当中来。实际上,我们可以做的还有很多。
要查看我对 Core Data 的 Swift 封装框架的话,可以在 这里 查看我的 GitHub。
问:你在实际产品中使用 Swift 的感受如何? Jesse:网上关于这方面的文章很多,但是我至今还没有在实际产品中使用 Swift。我觉得苹果更新这门语言的速度很快,并且在实际产品中使用 Swift 的话会带来很多优点。或许使用 Swift 最大的缺点就是第三方库的缺乏了,而这些三方库往往可以给开发带来极大地便利。就本次演讲而言,我认为 Swift 比起 Objective-C 要好用很多,尤其是指定构造器的使用。
问:这些新技术是否适用于现有基于 Core Data 撰写的应用呢?有必要将所有代码替换为 Swift 吗? Jesse:这些新特性并不足以让你替换所有代码,这不是一个充分的理由。替换代码得取决于你的实际情况,并且也取决于你是否能从这些新特性中获取到好处。因为从 Objective-C 代码迁移到 Swift 代码要花费极大的精力和时间。还有一点是我们所介绍的这些新特性并不能和 Objective-C 很好地兼容。因此,如果你只是简单的重写了数据模型,你可能会在代码的其余部分遇见问题,你会发现你根本无法访问这些东西!因此你就得不停地修改,以让你的代码能够适应它。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- RecyclerView使用指南(一)—— 基本使用
- 如何使用Meteorjs使用URL参数
- 使用 defer 还是不使用 defer?
- 使用 Typescript 加强 Vuex 使用体验
- [译] 何时使用 Rust?何时使用 Go?
- UDP协议的正确使用场合(谨慎使用)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。