如何在 Core Data 中使用 swift 4 Codable?

How to use swift 4 Codable in Core Data?

Codable 似乎是一个非常令人兴奋的功能。但我想知道我们如何在 Core Data 中使用它?特别是,是否可以直接 encode/decode a JSON from/to a NSManagedObject?

我尝试了一个非常简单的例子:

我自己定义了Foo

import CoreData

@objc(Foo)
public class Foo: NSManagedObject, Codable {}

但是这样使用时:

let json = """
{
    "name": "foo",
    "bars": [{
        "name": "bar1",
    }], [{
        "name": "bar2"
    }]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let foo = try! decoder.decode(Foo.self, from: json)
print(foo)

编译器失败并出现此错误:

super.init isn't called on all paths before returning from initializer

并且目标文件是定义 Foo

的文件

我想我可能做错了,因为我什至没有通过 NSManagedObjectContext,但我不知道该把它贴在哪里。

Core Data 是否支持Codable

CoreData 是它自己的持久性框架,根据其详尽的文档,您必须使用其指定的初始化器并遵循相当特定的路径来使用它创建和存储对象。

您仍然可以像使用 NSCoding 一样以有限的方式使用 Codable

一种方法是使用这些协议中的任何一个解码对象(或结构)并将其属性传输到您根据 Core Data 文档创建的新 NSManagedObject 实例中。

另一种方法(很常见)是仅对要存储在托管对象属性中的非标准对象使用其中一种协议。 "non-standard",我的意思是任何不符合模型中指定的 Core Data 标准属性类型的东西。例如,NSColor 不能直接存储为托管对象 属性,因为它不是 CD 支持的基本属性类型之一。相反,您可以使用 NSKeyedArchiver 将颜色序列化为 NSData 实例并将其作为数据 属性 存储在托管对象中。使用 NSKeyedUnarchiver 反转此过程。这很简单,并且有更好的方法可以使用 Core Data(请参阅 Transient Attributes)来执行此操作,但它说明了我的观点。

您也可以想象采用 Encodable(组成 Codable 的两个协议之一 - 您能猜出另一个的名称吗?)将托管对象实例直接转换为 JSON 用于共享,但您必须 specify coding keys 和您自己的自定义 encode 实现,因为编译器不会使用自定义编码键自动合成它。在这种情况下,您希望仅 指定您想要包含的键(属性)。

希望对您有所帮助。

您可以将 Codable 接口与 CoreData 对象一起使用来编码和解码数据,但是它不像与普通旧 swift 对象一起使用时那样自动。以下是如何直接使用 Core Data 对象实现 JSON 解码:

首先,让您的对象实现 Codable。此接口必须在对象上定义,而不是在扩展中定义。您还可以在此 class.

中定义您的编码键
class MyManagedObject: NSManagedObject, Codable {
    @NSManaged var property: String?

    enum CodingKeys: String, CodingKey {
       case property = "json_key"
    }
}

接下来,您可以定义init方法。这也必须在class方法中定义,因为Decodable协议需要init方法。

required convenience init(from decoder: Decoder) throws {
}

但是,用于托管对象的正确初始化程序是:

NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)

所以,这里的秘诀是使用 userInfo 字典将正确的上下文对象传递到初始化程序中。为此,您需要使用新密钥扩展 CodingUserInfoKey 结构:

extension CodingUserInfoKey {
   static let context = CodingUserInfoKey(rawValue: "context")
}

现在,您可以作为上下文的解码器:

required convenience init(from decoder: Decoder) throws {

    guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
    guard let entity = NSEntityDescription.entity(forEntityName: "MyManagedObject", in: context) else { fatalError() }

    self.init(entity: entity, in: context)

    let container = decoder.container(keyedBy: CodingKeys.self)
    self.property = container.decodeIfPresent(String.self, forKey: .property)
}

现在,当您为托管对象设置解码时,您需要传递正确的上下文对象:

let data = //raw json data in Data object
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[.context] = context

_ = try decoder.decode(MyManagedObject.self, from: data) //we'll get the value from another context using a fetch request later...

try context.save() //make sure to save your data once decoding is complete

要对数据进行编码,您需要使用 encode 协议函数执行类似的操作。

Swift 4.2:

按照casademora的解决方案,

guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }

应该是

guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }.

这可以防止 Xcode 错误地识别为数组切片问题的错误。

编辑:使用隐式解包选项来消除每次使用时强制解包的需要 .context

对于那些想使用 XCode 的现代方法来生成 NSManagedObject 文件的人来说,我创建了一个 DecoderWrapper class公开一个 Decoder 对象,然后我在我的对象中使用它符合 JSONDecoding 协议:

class DecoderWrapper: Decodable {

    let decoder:Decoder

    required init(from decoder:Decoder) throws {

        self.decoder = decoder
    }
}

protocol JSONDecoding {
     func decodeWith(_ decoder: Decoder) throws
}

extension JSONDecoding where Self:NSManagedObject {

    func decode(json:[String:Any]) throws {

        let data = try JSONSerialization.data(withJSONObject: json, options: [])
        let wrapper = try JSONDecoder().decode(DecoderWrapper.self, from: data)
        try decodeWith(wrapper.decoder)
    }
}

extension MyCoreDataClass: JSONDecoding {

    enum CodingKeys: String, CodingKey {
        case name // For example
    }

    func decodeWith(_ decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decode(String.self, forKey: .name)
    }
}

这可能只对没有任何非可选属性的模型有用,但它解决了我想使用 Decodable 的问题,而且还管理了与核心数据的关系和持久性,而无需手动创建我所有的 classes /属性。

编辑:使用示例

如果我有一个 json 对象:

let myjson = [ "name" : "Something" ]

我在 Core Data 中创建对象(为简洁起见强制转换):

let myObject = NSEntityDescription.insertNewObject(forEntityName: "MyCoreDataClass", into: myContext) as! MyCoreDataClass

我使用扩展让对象解码 json:

do {
    try myObject.decode(json: myjson)
}
catch {
    // handle any error
}

现在 myObject.name"Something"