在 Swift 中使用 Core Data

栏目: Swift · 发布时间: 7年前

内容简介:在 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 很好地兼容。因此,如果你只是简单的重写了数据模型,你可能会在代码的其余部分遇见问题,你会发现你根本无法访问这些东西!因此你就得不停地修改,以让你的代码能够适应它。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

软件开发本质论

软件开发本质论

Ron Jeffries / 王凌云 / 人民邮电出版社图灵分社 / 2017-1 / 39

想象你正在攀登一座名为“软件开发”的山峰。本书是与你同登一座山峰的敏捷先驱所带来的话语与图片。他在崎岖的山路边找到相当平坦的歇脚处,画下所见的风景,并写下自己的想法和发现。他瞧见很多条上山的路,愿以此书与你分享哪条路容易、哪条路困难、哪条路安全、哪条路危险。他还想指引你欣赏身后的美景。正是这些美景丰富了你的登山之旅,让你在重重困难中收获成长。 “对于每一位CTO、技术VP、软件产品总......一起来看看 《软件开发本质论》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试