Codable semble une fonctionnalité très excitante. Mais je me demande comment nous pouvons l'utiliser dans Core Data? En particulier, est-il possible de coder/décoder directement un JSON de/vers un NSManagedObject?
J'ai essayé un exemple très simple:
et défini Foo
moi-même:
import CoreData
@objc(Foo)
public class Foo: NSManagedObject, Codable {}
Mais lorsque vous l'utilisez comme ceci:
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)
Le compilateur a échoué avec cette erreur:
super.init isn't called on all paths before returning from initializer
et le fichier cible était le fichier qui a défini Foo
Je suppose que je l’ai probablement mal fait, car je n’ai même pas passé une NSManagedObjectContext
, mais je ne sais pas trop où la coller.
Les données de base prennent-elles en charge Codable
?
Vous pouvez utiliser l'interface Codable avec des objets CoreData pour encoder et décoder des données. Toutefois, ce n'est pas aussi automatique que lorsqu'il est utilisé avec de vieux objets Swift. Voici comment vous pouvez implémenter le décodage JSON directement avec des objets Core Data:
Tout d'abord, vous rendez votre objet implémentable Codable. Cette interface doit être définie sur l'objet et non dans une extension. Vous pouvez également définir vos clés de codage dans cette classe.
class MyManagedObject: NSManagedObject, Codable {
@NSManaged var property: String?
enum CodingKeys: String, CodingKey {
case property = "json_key"
}
}
Ensuite, vous pouvez définir la méthode init. Cela doit également être défini dans la méthode class car la méthode init est requise par le protocole Decodable.
required convenience init(from decoder: Decoder) throws {
}
Cependant, l'initialiseur approprié pour une utilisation avec des objets gérés est:
NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)
Le secret consiste donc à utiliser le dictionnaire userInfo pour transmettre l'objet de contexte approprié à l'initialiseur. Pour ce faire, vous devrez étendre la structure CodingUserInfoKey
avec une nouvelle clé:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
Maintenant, vous pouvez juste comme décodeur pour le contexte:
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)
}
Maintenant, lorsque vous configurez le décodage pour les objets gérés, vous devez transmettre l'objet de contexte approprié:
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
Pour encoder des données, vous devrez faire quelque chose de similaire en utilisant la fonction encoder protocole.
CoreData est son propre framework de persistance et, conformément à sa documentation exhaustive, vous devez utiliser ses initialiseurs désignés et suivre un chemin plutôt spécifique pour créer et stocker des objets avec ce dernier.
Vous pouvez toujours utiliser Codable
avec elle de manière limitée, tout comme vous pouvez utiliser NSCoding
.
Une solution consiste à décoder un objet (ou une structure) avec l'un de ces protocoles et à transférer ses propriétés dans une nouvelle instance NSManagedObject
créée par la documentation de Core Data.
Une autre méthode (très courante) consiste à utiliser l'un des protocoles uniquement pour un objet non standard que vous souhaitez stocker dans les propriétés d'un objet géré. Par "non standard", j'entends tout ce qui n'est pas conforme aux types d'attributs standard de Core Data tels que spécifiés dans votre modèle. Par exemple, NSColor
ne peut pas être stocké directement en tant que propriété d'objet géré, car ce n'est pas l'un des types d'attributs de base pris en charge par le CD. À la place, vous pouvez utiliser NSKeyedArchiver
pour sérialiser la couleur dans une instance NSData
et la stocker en tant que propriété de données dans l'objet géré. Inversez ce processus avec NSKeyedUnarchiver
. C'est simpliste et il y a une bien meilleure façon de faire cela avec Core Data (voir Attributs de transitoires ) mais cela illustre mon propos.
Vous pouvez également éventuellement adopter Encodable
(l'un des deux protocoles qui composent Codable
- pouvez-vous deviner le nom de l'autre?) Pour convertir une instance d'objet géré directement en JSON pour le partage, mais vous devez spécifier des clés de codage et votre propre implémentation encode
personnalisée, car elle ne sera pas synthétisée automatiquement par le compilateur avec des clés de codage personnalisées. Dans ce cas, vous souhaitez spécifier uniquement les clés (propriétés) à inclure.
J'espère que cela t'aides.
Comme solution de rechange pour ceux qui souhaitent utiliser l'approche moderne de XCode en matière de génération de fichiers NSManagedObject
, j'ai créé une classe DecoderWrapper
pour exposer un objet Decoder
que j'utilise ensuite dans mon objet et qui est conforme à un protocole 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)
}
}
Cela n’est probablement utile que pour les modèles sans attributs non facultatifs, mais cela résout mon problème de vouloir utiliser Decodable
mais aussi de gérer les relations et la persistance avec Core Data sans avoir à créer manuellement toutes mes classes/propriétés.