CoreValue 0.3.0

CoreValue 0.3.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2016 年 10 月
SPM支持 SPM

Benedikt Terhechte 维护。



CoreValue 0.3.0


功能

  • 使用 Swift 反射将值类型转换为 NSManagedObject
  • 支持 iOS 和 Mac OS X
  • 可用于 structs
  • 适用于基于 letvar 的属性
  • Swift 3.0 (对于 Swift 2.2,请使用 版本 0.2)

合理性

Swift 在 iOS 和 Cocoa 开发领域引入了多才多艺的值类型。它们轻巧、快速、安全、强制不可变性等等。然而,一旦项目中出现对 CoreData 的需求,我们就不得不回到引用类型和 @objc

CoreValue 是围绕 Core Data 的一轻量级包装框架。它负责将值类型装箱到 Core Data 对象中,并将 Core Data 对象拆箱到值类型中。它还包含用于简单查询、更新、保存和删除的简单抽象。

如果您正在将您的应用程序移植到 Swift 3,请参阅下面的 Swift 3 部分。

使用方法

以下结构支持装箱、拆箱和保持对象状态

struct Shop: CVManagedPersistentStruct {

    // The name of the CoreData entity
    static let EntityName = "Shop"

    // The ObjectID of the CoreData object we saved to or loaded from
    var objectID: NSManagedObjectID?

    // Our properties
    let name: String
    var age: Int32
    var owner: Owner?

    // Create a Value Type from a NSManagedObject
    // If this looks too complex, see below for an explanation and alternatives
    static func fromObject(_ o: NSManagedObject) throws -> XShop {
        return try curry(self.init)
            <^> o <|? "objectID"
            <^> o <| "name"
            <^> o <| "age"
            <^> o <|? "owner"
    }
}

这就完成了。从现在开始,所有其他内容都是自动化的。下面是一些使用 Shop 可以做的示例

    // Get all shops (`[Shop]` is required for the type checker to get your intent!)
    let shops: [Shop] = Shop.query(self.context, predicate: nil)

    // Create a shop
    let aShop = Shop(objectID: nil, name: "Household Wares", age: 30, owner: nil)

    // Store it as a managed object
    aShop.save(self.context)

    // Change the age
    aShop.age = 40

    // Update the managed object in the store
    aShop.save(self.context)

    // Delete the object
    aShop.delete(self.context)

    // Convert a managed object into a shop (see below)
    let nsShop: Shop? = try? Shop.fromObject(aNSManagedObject)

    // Convert a shop into an nsmanagedobject
    let shopObj = nsShop.mutatingToObject(self.context)

查询

有两种方法从 Core Data 查询对象到值

// With Sort Descriptors
public static func query(context: NSManagedObjectContext, predicate: NSPredicate?, sortDescriptors: Array<NSSortDescriptor>) -> Array

// Without sort descriptors
public static func query(context: NSManagedObjectContext, predicate: NSPredicate?) -> Array

如果没有提供 NSPredicate,则返回所选实体的所有对象。

详细使用方法

CVManagedPersistentStruct 是 CoreValue 的两个主要协议的类型别名:BoxingPersistentStructUnboxingStruct

让我们看看它们做什么。

BoxingPersistentStruct

装箱是将值类型转换成 NSManagedObject 的过程。CoreValue 真的爱你们,这就是为什么它通过 Swift 的 Reflection 功能为您做所有艰难的工作。请亲自看看

struct Counter : BoxingStruct
    static let EntityName = "Counter"
    var count: Int
    let name: String
}

就是这样。您的值类型现在符合 Core Data。只需调用 aCounter.toObject(context),您就会得到一个正确编码的 NSManagedObject

如果您感兴趣,可以查看 CoreValue.swift 中的 internalToObject 函数,该函数负责此操作。

装箱细节

细心的观察者可能会注意到,上面的结构实际上并没有实现 BoxingPersistentStruct 协议,而是实现了一个名为 BoxingStruct 的不同协议,那这里发生了什么?

默认情况下,值类型是不可变的,所以即使你将一个属性定义为 var,也无法在内部进行更改,除非你声明你的函数是可变的。Swift 还不允许我们在协议扩展中定义属性,因此我们希望分配到值类型的任何状态都必须通过值类型的特定属性来实现。

当我们从 Core Data 创建或加载 NSManagedObject 时,我们需要一种方法在值类型中存储到原始 NSManagedObject 的连接。否则,再次调用 save(比如在更新值类型之后),不会更新相关的 NSManagedObject,而会向存储中插入一个新的 NSManagedObject。这显然不是我们想要的。

由于我们不能隐式地向任何协议添加任何状态,我们必须明确定义这一点。这就是为什么有一个用于持久存储的独立协议的原因。

struct Counter : BoxingPersistentStruct
    let EntityName = "Counter"

    var objectID: NSManagedObjectID?

    var count: Int
    let name: String
}

这里的主要区别是添加了 objectID 属性。一旦存在这个属性,就可以使用 BoxingPersistentStruct 的所有神奇功能(.save, .delete, .mutatingToObject)。

那么,你可能想知道 BoxingStruct 协议的用例是什么。优势是 BoxingStruct 不需要你的值类型是可变的,并且默认情况下不会以任何可变函数扩展它,保持其为真正的不可变值类型。它仍然可以使用 .toObject 将值类型转换为 NSManagedObject,但由于它无法修改这个对象,因此它仍然适用于所有只需要进行插入(如缓存或日志)的场景,或者在批量执行修改(删除所有)的场景,或者直接在 NSManagedObject 它本身上进行更新(.valueForKey, .save)的场景。

装箱和子属性

一条建议:如果你在值类型内部有值类型,例如

struct Employee : BoxingPersistentStruct {
    let EntityName = "Employee"
    var objectID: NSManagedObjectID?
    let name: String
}

struct Shop : BoxingPersistentStruct {
    let EntityName = "Counter"
    var objectID: NSManagedObjectID?
    let employees: [Employee]
}

那么你需要确保所有值类型都符合相同的装箱协议,无论是 BoxingPersistentStruct 还是 BoxingStruct。类型检查器无法检查这一点,并报告错误。

短暂的对象

在 CoreValue 中,大多数协议将 NSManagedObjectContext 标记为可选的,这意味着你不需要提供它。装箱仍然会按预期工作,只是生成的 NSManagedObject 将是短暂的,即它们没有绑定到上下文,无法存储。这种用例很少,但需要注意的是,不提供 NSManagedObjectContext 不会导致错误。

UnboxingStruct

在 CoreValue 中,boxed 指的是 NSManagedObject 容器中的值。即 NSNumber 将 Int 装箱,NSOrderedSet 将 Array 装箱,而 NSManagedObject 本身将值类型(例如 Shop)装箱。

UnboxingStruct 可以应用于任何旨在从 NSManagedObject 初始化的结构或类。它只有一个需要实现的条件,那就是 fromObject,它接收一个 NSManagedObject 并应返回一个值类型。以下是一个非常简单且不安全的示例

struct Counter : UnboxingStruct
    var count: Int
    let name: String
    static func fromObject(_ object: NSManagedObject) throws -> Counter {
    return Counter(count: object.valueForKey("count")!.integerValue,
           name: object.valueForKey("name") as! String)
    }
}

尽管这个示例不安全,我们仍可以从中学到一些东西。首先,实现开销最小。其次,这种方法可能会抛出一个错误。这是因为解箱可以以多种方式失败(错误值、无值、错误实体、未知实体等)。如果解箱在任何方式下失败,我们会抛出一个 NSError。解箱的另一个好处是它允许我们取巧(CoreValue狡猾地复制了Argo)。利用多个自定义运算符,解箱过程可以大大简化。

struct Counter : UnboxingStruct
    var count: Int
    let name: String
    static func fromObject(_ object: NSManagedObject) throws -> Counter {
        return try curry(self.init) <^> object <| "count" <*> object <| "name"
    }
}

此代码使用自动初始化器,将其柯里化并映射到多个解箱函数的化身(<|),直到它可以返回一个计数器(或抛出错误)。

那么这些奇怪的符文到底是怎么回事呢?下面是对这里发生的事情的详细介绍。

解箱详细探讨

curry(self.init)

(A, B) -> T 转换为 A -> B -> C 以便于分步骤调用

<^> 将以下操作映射到我们刚刚创建的 A -> B -> fn

object <| "count" 第一次操作:取 object,使用键 "count" 调用 valueForKey 并将此作为柯里化初始化函数的第一种类型的值为

object <| "name" 第二次操作:取 object,使用键 "name" 调用 valueForKey 并将此作为柯里化初始化函数的第二种类型的值

其他运算符

自定义运算符被视为 Swift 的一项关键特性,这是有其原因的。太多的那些会使代码库难以阅读和理解。以下自定义运算符与其他几个 Swift 框架中的运算符相同(见 Runes 和 Argo)。它们基本上是直接从 Haskell 复制过来的,所以尽管这并不使它们更具有或更正式,但它们至少在非官方上是达成共识的。

<| 并不是编码对象的唯一运算符。以下列出所有支持的运算符

运算符 描述
<^> 映射以下操作(即组合映射操作)
<| 解箱正常值(即 var shop: Shop)
<|| 解箱值集/列表(即 var shops: [Shop])
<|? 解箱可选值(即 var shop: Shop?)

CVManagedStruct

由于您几乎总是需要装箱和解箱功能,CoreValue 包含两个方便的类型别名,即 CVManagedStructCVManagedPersistentStruct,它们将装箱和解箱功能包含在一个类型中。

RawRepresentable 枚举支持

通过扩展 RawRepresentable,您可以直接使用 Swift enums,而无需首先确保您的枚举符合 CVManagedStruct

enum CarType:String{
    case Pickup = "pickup"
    case Sedan = "sedan"
    case Hatchback = "hatchback"
}

extension CarType: Boxing,Unboxing {}

 struct Car: CVManagedPersistentStruct {
     static let EntityName = "Car"
     var objectID: NSManagedObjectID?
     var name: String
     var type: CarType

     static func fromObject(_ o: NSManagedObject) throws -> Car {
         return try curry(self.init)
             <^> o <|? "objectID"
             <^> o <| "name"
             <^> o <| "type"
     }
}

文档

查看CoreValue.swift,它充满了文档字符串

或者,您可以在单元测试中找到很多用法。

这是一个更复杂的使用 CoreValue 的例子

struct Employee : CVManagedPersistentStruct {

    static let EntityName = "Employee"

    var objectID: NSManagedObjectID?

    let name: String
    var age: Int16
    let position: String?
    let department: String
    let job: String

    static func fromObject(_ o: NSManagedObject) throws -> Employee {
        return try curry(self.init)
            <^> o <| "objectID"
            <^> o <| "name"
            <^> o <| "age"
            <^> o <|? "position"
            <^> o <| "department"
            <^> o <| "job"
    }
}

struct Shop: CVManagedPersistentStruct {
    static let EntityName = "Shop"

    var objectID: NSManagedObjectID?

    var name: String
    var age: Int16
    var employees: [Employee]

    static func fromObject(_ o: NSManagedObject) throws -> Shop {
        return try curry(self.init)
            <^> o <| "objectID"
            <^> o <| "age"
            <^> o <| "name"
            <^> o <|| "employees"
    }
}

// One year has passed, update the age of our shops and employees by one
let shops: [Shop] = Shop.query(self.managedObjectContext, predicate: nil)
for shop in shops {
    shop.age += 1
    for employee in shop.employees {
        employee.age += 1
    }
    shop.save()
}

CVManagedUniqueStruct 和 REST / 序列化 / JSON

到目前为止,我们看到的所有示例都围绕着在应用中包含数据的使用情况。这意味着NSManagedObject或Struct的唯一标识符由CoreData生成的NSManagedObjectID唯一标识符决定。只要你不打算与外部数据交互,这是可以的。如果你的数据是从外部源(例如REST API的JSON)加载的,则它可能已经具有一个唯一标识符。CVManagedUniqueStruct 允许你强制CoreValue / Core Data使用此外部唯一标识符而不是NSManagedObjectID。实现起来很简单。你只需要遵从 BoxingUniqueStruct 协议,该协议要求实现一个命名唯一id字段的var和一个返回当前ID值的函数

/** Name of the Identifier in the CoreData (e.g: 'id')
  */
static var IdentifierName: String {get}

/** Value of the Identifier for the current struct (e.g: 'self.id')
  */
func IdentifierValue() -> IdentifierType

以下是一个完整且简单的示例

struct Author : CVManagedUniqueStruct {

    static let EntityName = "Author"

    static var IdentifierName: String = "id"

    func IdentifierValue() -> IdentifierType { return self.id }

    let id: String
    let name: String

    static func fromObject(_ o: NSManagedObject) throws -> Author {
        return try curry(self.init)
            <^> o <| "id"
            <^> o <| "name"
    }
}

请注意,由于当前对象查找的实现方式,CVManagedUniqueStructNSManagedObjectID基于的解决方案上添加了一个约O(n)的开销。

状态

所有Core Data数据类型都受支持,但有以下 例外

  • 可转换的
  • 无序集合 / NSSet(目前,仅支持有序集合)

尚未支持获取属性。

Swift 3.0 转换

Swift 3.0转换在框架内更改了一些东西。为了简化,这里是一个要做的列表

  1. <*> 操作符替换成 <^>
  2. func fromObject(object) 替换成 func fromObject(_ object)
  3. return curry(self.init)... 替换成 return try curry(self.init)...

安装(iOS和OSX)

手动

  1. 将CoreValue.swift和curry.swift文件复制到您的项目中。
  2. 将Core Data框架添加到您的项目中

手动安装时,无需导入 import CoreValue

联系

Benedikt Terhechte

@terhechte

Appventure.me

更新日志

版本 0.3.0

  • Swift 3 支持
  • 添加了 CVManagedUniqueStruct,归功于 tkohout

版本 0.2.0

  • 将错误处理从 Unboxed 更改为Swift的原生 throw。感谢 Adlai Holler 成功发起此事!
  • 性能大幅提升:装箱大约快80%,解装箱大约快90%
  • 感谢 Roman Kříž,提高了嵌套集合的支持性。
  • 支持 RawRepresentable(见上面的文档),归功于 tkohout

版本 0.1.6

  • 使CVManagedPersistentStruct公开
  • 解决了空集合的问题

版本 0.1.4

包括了来自AlexanderKaraberov的代码审查请求,其中包含了删除函数的修复

版本 0.1.3

更新到最新的Swift 2.0 b4更改

版本 0.1.2

将 NSManagedStruct 以及 NSPersistentManagedStruct 重命名为 CVManagedStruct 和 CVPersistentManagedStruct ,因为 NS 是保留的前缀用于 Apple 的类

版本 0.1.1

增加了 CocoaPods 支持

版本 0.1.0

初次发布

致谢

CoreValue 使用了来自 thoughtbot 的 Argo 框架用于 JSON 解码 的想法和代码。其中最著名的是他们的 curry 实现。去看看它,这是一个很棒的框架。

许可

CoreValue 源代码可根据 MIT 许可证使用。

待办事项

  • [ ] 测试使用自定义初始化器(init(…))的解包
  • [ ] 更改协议组合,使所需实现(entityname, objectID, fromObject)形成一个空的协议,这样更容易看到协议并实现需求
  • [ ] 添加 travis 构建
  • [ ] 支持聚合
  • [ ] 添加对 nsset / 无序列表的支持
  • [ ] 添加对已获取属性的(可能是类似于 (objects, predicate) 的结构)支持
  • [ ] 支持 transformable: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdNSAttributes.html
  • [ ] 添加 jazzy 进行文档编写并更新头文件以包含适当的文档
  • [ ] 通过 objectID 记录多线程支持