structs
let
和 var
的属性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 的两个主要协议的类型别名:BoxingPersistentStruct
,UnboxingStruct
。
让我们看看它们做什么。
装箱是将值类型转换成 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 不会导致错误。
在 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?) |
由于您几乎总是需要装箱和解箱功能,CoreValue 包含两个方便的类型别名,即 CVManagedStruct
和 CVManagedPersistentStruct
,它们将装箱和解箱功能包含在一个类型中。
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()
}
到目前为止,我们看到的所有示例都围绕着在应用中包含数据的使用情况。这意味着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"
}
}
请注意,由于当前对象查找的实现方式,CVManagedUniqueStruct
在NSManagedObjectID
基于的解决方案上添加了一个约O(n)的开销。
所有Core Data数据类型都受支持,但有以下 例外
尚未支持获取属性。
Swift 3.0转换在框架内更改了一些东西。为了简化,这里是一个要做的列表
<*>
操作符替换成 <^>
func fromObject(object)
替换成 func fromObject(_ object)
return curry(self.init)...
替换成 return try curry(self.init)...
手动安装时,无需导入 import CoreValue
。
Benedikt Terhechte
Unboxed
更改为Swift的原生 throw
。感谢 Adlai Holler 成功发起此事!CVManagedPersistentStruct
公开包括了来自AlexanderKaraberov的代码审查请求,其中包含了删除函数的修复
更新到最新的Swift 2.0 b4更改
将 NSManagedStruct 以及 NSPersistentManagedStruct 重命名为 CVManagedStruct 和 CVPersistentManagedStruct ,因为 NS 是保留的前缀用于 Apple 的类
增加了 CocoaPods 支持
初次发布
CoreValue 使用了来自 thoughtbot 的 Argo 框架用于 JSON 解码 的想法和代码。其中最著名的是他们的 curry
实现。去看看它,这是一个很棒的框架。
CoreValue 源代码可根据 MIT 许可证使用。