如何在 Core Data 中使用 swift 4 Codable?
Posted
技术标签:
【中文标题】如何在 Core Data 中使用 swift 4 Codable?【英文标题】:How to use swift 4 Codable in Core Data? 【发布时间】:2017-06-09 05:45:50 【问题描述】:可编码似乎是一个非常令人兴奋的功能。但我想知道我们如何在 Core Data 中使用它?特别是,是否可以直接从 NSManagedObject 编码/解码 JSON?
我尝试了一个非常简单的例子:
并自己定义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
?
【问题讨论】:
可以在here找到一个使用公认答案的好例子 【参考方案1】:您可以使用 Codable 接口和 CoreData 对象来编码和解码数据,但是它不像使用普通的旧 swift 对象那样自动。以下是使用 Core Data 对象直接实现 JSON 解码的方法:
首先,你让你的对象实现 Codable。此接口必须在对象上定义,而不是在扩展中。你也可以在这个类中定义你的编码键。
class MyManagedObject: NSManagedObject, Codable
@NSManaged var property: String?
enum CodingKeys: String, CodingKey
case property = "json_key"
接下来,您可以定义 init 方法。这也必须在类方法中定义,因为可解码协议需要 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 协议函数执行类似的操作。
【讨论】:
好主意。有没有办法以这种方式初始化然后更新现有对象?例如检查 id 是否已经在 CoreData 中。如果存在,则加载对象并更新,否则创建新对象(如上所述)。 我要补充一点,由于从 JSON 解码值可能会抛出错误,因此即使 JSON 解码遇到错误,此代码也可能允许将对象插入上下文中。您可以从调用代码中捕获它并通过删除刚刚插入但抛出的对象来处理它。 嘿,我遵循了与上述相同的步骤,但即使托管对象在解码后具有值,context.hasChanges 也总是给我错误。由于没有更改 context.save 没有保存。我试图直接调用 context.save,我没有错误地通过但数据库是空的。我还检查了传递给解码器的上下文指针,它是相同的。有什么想法吗? @Tarang 你成功了吗?我这里也有同样的问题,数据库是空的,上下文没有持久化。 如果您想更新现有对象,此答案无济于事。它总是创建新对象并复制您现有的记录。【参考方案2】:CoreData 是它自己的持久性框架,根据其详尽的文档,您必须使用其指定的初始化程序并遵循相当具体的路径来创建和存储对象。
您仍然可以使用Codable
以有限的方式使用它,就像您可以使用NSCoding
一样,但是。
一种方法是使用这些协议中的任何一个解码对象(或结构)并将其属性传输到您根据 Core Data 文档创建的新 NSManagedObject
实例。
另一种方法(非常常见)是仅将其中一种协议用于要存储在托管对象属性中的非标准对象。 “非标准”是指任何不符合模型中指定的 Core Data 标准属性类型的东西。例如,NSColor
不能直接存储为托管对象属性,因为它不是 CD 支持的基本属性类型之一。相反,您可以使用NSKeyedArchiver
将颜色序列化为NSData
实例并将其作为数据属性存储在托管对象中。使用NSKeyedUnarchiver
反转此过程。这很简单,并且有更好的方法可以使用 Core Data 来做到这一点(请参阅Transient Attributes),但这说明了我的观点。
您也可以采用Encodable
(构成Codable
的两个协议之一 - 您能猜出另一个的名称吗?)将托管对象实例直接转换为 JSON 以进行共享,但您必须这样做specify coding keys 和您自己的自定义 encode
实现,因为编译器不会使用自定义编码键自动合成它。在这种情况下,您希望仅指定要包含的键(属性)。
希望这会有所帮助。
【讨论】:
感谢您的详细解释。我目前正在使用您提到的第一种方法。但是我真的希望NSManagedObject
可以默认符合Codable
,有json = encoder.encode(foo)
这样的方法可以直接编码,foo = decoder.decode(Foo.self, json, context)
可以直接解码。希望在更新或下一个主要版本中看到它。
我真的不会指望它。自定义编码/解码的能力几乎涵盖了应用商店之间数据传输的所有基础,以及仅使用 JSON 解码器的大多数实际案例。由于这两种方法对于应用程序持久性是相互排斥的(由于它们完全不同的设计方法和用例),因此这种支持的可能性几乎为零。但希望永远涌现。 ;-)
@JoshuaNozzi 我完全不同意这个评论。您可以相当轻松地更改映射,并且人们使用库来实现这种特定方法。如果将来支持 2 次左右的 ios 迭代,我不会感到惊讶。它只需要对协议进行调整以支持无需初始化的填充,或者与 CoreData 当前初始化接口和 CoreData 模型的 Codable 枚举代码生成(它们已经具有代码生成)的基本级别一致性。这些方法并不相互排斥,99% 使用核心数据的应用都在映射 JSON。
@TheCodingArt 你指的是什么?自定义商店类型?除了 Core Data 机制之外,这与直接在单个托管对象上直接使用 Codable/Decodable 有点不同。
@JoshuaNozzi 我从来没有引用过任何关于自定义存储类型的东西......这是一个简单的 Swift 中属性的序列化/反序列化映射,带有可编码代码生成的键值。【参考方案3】:
斯威夫特 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
的需要。
【讨论】:
我宁愿让静态常量 (.context) 强制在定义处展开,而不是像这样将它洒在整个源代码上。 @casademora 这与您的答案相同,仅适用于 swift 4.2(编辑:我明白您的意思。隐式展开的选项:-)。 )。 是的,我知道其中的区别。我只是建议将解包放在常量上(在一个地方),而不是 userInfo 访问器(可能无处不在) 嗨,我正在编码(在 iOS 应用程序中)和解码(在手表套件扩展中),你是如何设法在那里获得相同的上下文的?【参考方案4】:对于那些想利用 XCode 的现代方法来生成 NSManagedObject
文件的人的替代方案,我创建了一个 DecoderWrapper
类来公开一个 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
的问题,而且还可以管理与 Core Data 的关系和持久性,而无需手动创建我的所有类/属性。
编辑:使用示例
如果我有一个 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"
【讨论】:
如果我们有一个像@NSManaged public var products: NSSet 的自定义对象。我们将如何解码这个对象。 您可以将其转换为可编码的常规集合以上是关于如何在 Core Data 中使用 swift 4 Codable?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Core Data 中使用 swift 4 Codable?
在Core Data中,如何在swift 4中执行`if exists update else insert`? [复制]
使用 Swift 使用 Core Data 对象填充 UIPickerView
IOS Swift Core Data如何在propertiesToFetch中添加不在propertiesToGroupBy中的字段