CoReDaTa ble 0.1.0

Coredatabase 0.1.0

Manuel García-Estañ 维护。



Coredatable

NSManagedObject 子类中实现简单的 Codable 兼容。

该库处于测试阶段。在发布 1.0.0 版本之前,可能会添加破坏性更改。

安装

CocoaPods

将以下内容添加到您的 Podfile

pod 'Coredatable'

总结

DecodableEncodable 兼容性添加到 NSManagedObject 子类通常很困难。使用名为 CoreDataDecodableCoreDataEncodableCoreDataCodable 的等效协议来简化此过程:

final class Person: NSManagedObject, CoreDataCodable, UsingDefaultCodingKeys {
    @NSManaged var id: Int
    @NSManaged var name: String?
    @NSManaged var city: String?
    @NSManaged var birthday: Date?
    @NSManaged var attributes: NSSet
}

class PersonAttribute: NSManagedObject, CoreDataCodable {    
    @NSManaged private(set) var id: Int
    @NSManaged private(set) var name: String
    
    enum CodingKeys: String, CoreDataCodingKey {
        case id
        case name = "attributeName"
    }
}

// Decode
let decoder = JSONDecoder()
decoder.managedObjectContext = myContext
let person = try decoder.decode(Person.self, from: data)

// Encode
let encoder = JSONEncoder()
let data = try encoder.encode(person)

是的,就这么简单。

您只需将一个 NSManagedObjectContext 添加到您的解码器中,并使类对应协议。该协议强制您添加一个 CodingKeys 类型。在我们的示例中,有两种不同的情况:

  • 如果使用默认键(与属性的名称相同),您只需对应 UsingDefaultCodingKeys。或者,您可以这样做:typealias CodingKeys = CoreDataDefaultCodingKeys
  • 如果您想使用一组不同的键,您必须创建一个名为 CodingKeysenum,使它对应 CoreDataCodingKeys,并定义其情况和字符串值。

如果您需要更定制的 CodingKey,您可以创建一个 classstruct 或其他任何类型,然后使其遵循 AnyCoreDataCodingKey

标识属性。

可选地,您可以在您的 NSManagedObject 实例中确保其唯一性。要这样做,您可以使用 identityAttribute 属性。

final class Person: NSManagedObject, CoreDataCodable, UsingDefaultCodingKeys {
    @NSManaged var id: Int
    @NSManaged var name: String?
    @NSManaged var country: String?
    @NSManaged var birthday: Date?
    @NSManaged var attributes: NSSet
    
    static let identityAttribute: IdentityAttribute = #keyPath(Person.id)
}

如果您这样做,Coredatable 将会负责检查是否有具有相同 identityAttribute 值的对象已经存在于上下文中。如果存在,该对象将使用新值进行更新。如果不存在,它将直接插入。当一个对象被更新时,只有新的 JSON 中存在的值会被更新。

以这种方式支持复合标识属性。

static let identityAttribute: IdentityAttribute = [#keyPath(Person.id), #keyPath(Person.name)]

然而,只有在确实需要时才使用复合标识属性,因为的性能将会受到影响。单独的标识属性策略需要在每个 JSON 对象数组上执行一次获取操作,而复合标识属性策略需要在每个单独的 JSON 对象上执行一次获取操作。

如果不需要唯一性,您甚至可以完全不包含 identityAttribute

从标识符序列化关系

假设我们的 API 不返回关系的完整对象,而是只返回标识符。

我们不需要更改我们的模型来支持这种情况。

let json: [String: Any] = [
    "id": "1",
    "name": "Marco",
    "attributes": [1, 2]
]
let data = try JSONSerialization.data(withJSONObject: json, options: [])
let person = try decoder.decode(Person.self, from: data)

上述代码创建了一个 Person 对象。如果我们已经在上下文中有具有 id 12PersonAttribute,那些对象将会被设置在关系上。如果没有,将会创建两个具有给定 idPersonAttribute 实例。

请注意,仅当实体指定 identityAttribute 的值仅由一个属性组成时,从标识符序列化关系才起作用。

KeyPath 编码键

假设我们有一个嵌套 JSON 中的值

{
    "id": 1,
    "name": "Marco",
    "origin": {
        "country" {
            "id": 1,
            "name": "Spain"
        }
    }
}

我们可以使用键路径符号直接访问国家名称

enum CodingKeys: String, CoreDataCodingKey {
    case id, name, birthday, attributes
    case country = "origin.country.name"
}

默认情况下,键路径使用点 ." 作为路径分隔符,但您可以使用任何其他字符串添加此操作

enum CodingKeys: String, CoreDataCodingKey {
    case id, name, birthday, attributes
    case country = "origin->country->name"
    
    var keyPathDelimiter: String { "->" }
}

自定义解码

如果您需要自定义序列化,您需要做一些不同于常规Codable的操作。而不是覆盖init(from decoder: Decoder),您应该覆盖func initialize(from decoder: Decoder) throws

让我们看一个示例

final class Custom: NSManagedObject, CoreDataDecodable {
    @NSManaged var id: Int
    @NSManaged var compound: String
    
    enum CodingKeys: String, CoreDataCodingKey {
        case id
        case first
        case second
    }
    
    static var identityAttribute: IdentityAttribute = #keyPath(Custom.id)
    
    // 1
    func initialize(from decoder: Decoder) throws {
        // 2
        try defaultInitialization(from: decoder, with: [.id])
        
        // 3
        let container = try decoder.container(for: Custom.self)
        
        // 4
        let first = try container.decode(String.self, forKey: .first)
        let second = try container.decode(String.self, forKey: .second)
        
        // 5
        compound = [first, second].joined(separator: " ")
    }
}

这里,我们有一个compound属性,它通过连接来自不同键的两个字符串创建。我们这样做的是

  • // 1: 覆盖 func initialize(from container: CoreDataKeyedDecodingContainer<Custom>)
  • // 2: 只考虑id键来调用默认序列化。 (注意:还有一些其他默认实现,您可以在其中指定包含或跳过的键)
  • // 3: 创建一个新容器,将自己的类型作为参数传递。传递另一个类型将导致错误。
  • // 4: 从容器中提取firstsecond`值。
  • // 5: 将连接后的字符串添加到compound属性中。

如果您需要在将JSON序列化之前对identityAttributes进行某些更改,您需要使用不同的方法。假设我们的JSON将id发送为String,但我们将它作为整数。我们可以使用以下方法修改值

// 1
static func container(for decoder: Decoder) throws -> AnyCoreDataKeyedDecodingContainer {
   // 2
   var container = try decoder.container(for: CustomDoubleId.self)
   
   // 3
   container[.id] = Int(try container.decode(String.self, forKey: .id)) ?? 0
   
   // 4
   return container
}
  • // 1: 覆盖 static func container(for decoder: Decoder) throws -> AnyCoreDataKeyedDecodingContainer
  • // 2: 创建一个新的可变容器,将自己的类型作为参数传递。传递另一个类型将导致错误。
  • // 3: 将值转换为所需的类型并将其分配给.id
  • // 4: 返回修改后的容器

多个

您可以在另一个Codable对象中嵌套不带有任何问题的CoreDataDecodable对象

struct LoginResponse: Codable {
    let token: String
    let user: Person
}

但是,如果您想引用一个CoreDataDecodable对象的数组

struct Response: Codable {
    let nextPage: String
    let previousPage: String
    let total: Int
    // don't do this
    let results: [Person]
}

最好使用Many而不是Array

struct Response: Codable {
    let nextPage: String
    let previousPage: String
    let total: Int
    let results: Many<Person>
}

使用Many将提高性能。ManyArray的替代品,可以以相同的方式使用。在任何情况下,您都可以使用many.array访问原始数组。

灵感

这个库在很大程度上受到了Groot的启发

作者

@ManueGE

许可

Coredatable是在MIT许可证下可用的。请参阅LICENSE