58 votes

Comment utiliser swift 4 Codable dans Core Data ?

Codable semble être une fonctionnalité très intéressante. Mais je me demande comment nous pouvons l'utiliser dans Core Data ? En particulier, est-il possible d'encoder/décoder directement un JSON depuis/vers un NSManagedObject ?

J'ai essayé un exemple très simple :

enter image description here

et défini Foo moi-même :

import CoreData

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

Mais en l'utilisant comme ça :

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 définissait Foo

J'ai probablement mal fait les choses, puisque je n'ai même pas passé une NSManagedObjectContext mais je ne sais pas où le mettre.

Les données de base permettent-elles Codable ?

0 votes

Un bon exemple qui utilise la réponse acceptée peut être trouvé aquí

97voto

casademora Points 15214

Vous pouvez utiliser le Codable interface avec CoreData objets d'encoder et de décoder des données, cependant il n'est pas aussi automatique que lorsqu'il est utilisé avec un bon vieux swift objets. Voici comment vous pouvez mettre en œuvre Décodage JSON directement avec la Base de Données des objets:

Tout d'abord, vous rendre votre objet de mettre en œuvre Codable. Cette interface doit être défini sur l'objet, et non pas 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éfinie dans la méthode de la classe parce que la méthode init est requis par la Décodable protocole.

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

Cependant, l'initialiseur correct pour une utilisation avec des objets gérés est:

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

Donc, le secret ici est d'utiliser le userInfo dictionnaire de passer dans le contexte de l'objet dans l'initialiseur. Pour ce faire, vous aurez besoin d'étendre l' CodingUserInfoKey struct avec une nouvelle clé:

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

Maintenant, vous pouvez juste que le 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 des Objets Gérés, vous aurez besoin de passer le long du contexte de l'objet:

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 les données, vous aurez besoin de faire quelque chose de similaire à l'aide de l' encoder protocole de fonction.

14 votes

Excellente idée. Existe-t-il un moyen d'initialiser puis de mettre à jour les objets existants de cette manière ? Par exemple, vérifier si l'identifiant est déjà dans CoreData. S'il existe, chargez l'objet et mettez-le à jour, sinon créez-en un nouveau (comme décrit ci-dessus).

0 votes

Merci pour ça, Saul !

1 votes

J'ajouterais que, puisque le décodage des valeurs à partir de JSON peut lancer, ce code permet potentiellement d'insérer des objets dans le contexte même si le décodage JSON a rencontré une erreur. Vous pourriez attraper cela à partir du code appelant et le gérer en supprimant l'objet qui vient d'être inséré mais qui lance une erreur.

13voto

Joshua Nozzi Points 38718

CoreData est son propre cadre de persistance et, selon sa documentation détaillée, vous devez utiliser ses initialisateurs désignés et suivre un chemin plutôt spécifique pour créer et stocker des objets avec lui.

Vous pouvez toujours utiliser Codable avec elle de manière limitée, tout comme vous pouvez utiliser NSCoding Cependant.

Une façon de procéder est de décoder un objet (ou une structure) avec l'un ou l'autre de ces protocoles et de transférer ses propriétés dans un nouvel objet de type NSManagedObject que vous avez créée selon les documents 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'un Managed Object car il ne fait pas partie des types d'attributs de base pris en charge par le CD. À la place, vous pouvez utiliser NSKeyedArchiver pour sérialiser la couleur dans un NSData et la stocker comme une propriété Data dans l'objet géré. Inversez ce processus avec NSKeyedUnarchiver . C'est simpliste et il existe une bien meilleure façon de le faire avec Core Data (voir Attributs transitoires ) mais cela illustre mon propos.

Vous pouvez également envisager d'adopter Encodable (l'un des deux protocoles qui composent Codable - pouvez-vous deviner le nom de l'autre ?) pour convertir une instance de Managed Object directement en JSON afin de la partager, mais vous devez spécifier les clés de codage et vos propres encode puisqu'elle ne sera pas auto-synthétisée par le compilateur avec des clés de codage personnalisées. Dans ce cas, il faut spécifier uniquement les clés (propriétés) que vous voulez inclure.

J'espère que cela vous aidera.

0 votes

Merci pour cette explication détaillée. J'utilise actuellement la première approche que vous avez mentionnée. Mais j'espère vraiment NSManagedObject peut se conformer à Codable par défaut, et il existe des méthodes comme json = encoder.encode(foo) pour le coder directement, et foo = decoder.decode(Foo.self, json, context) pour décoder directement. J'espère le voir dans une mise à jour ou dans la prochaine version majeure.

2 votes

Je ne compterais vraiment pas dessus. La possibilité de personnaliser l'encodage/décodage couvre à peu près toutes les bases du transfert de données entre le magasin de votre application et la majorité des cas réels avec le seul décodeur/encodeur JSON. Étant donné que les deux approches s'excluent mutuellement pour la persistance des applications (en raison de leurs approches de conception et de leurs cas d'utilisation radicalement différents), il n'y a pratiquement aucune chance qu'un tel support existe. Mais l'espoir est éternel ;-)

1 votes

@JoshuaNozzi Je ne suis pas du tout d'accord avec ce commentaire. Vous pouvez modifier les mappings assez facilement et les gens utilisent des bibliothèques pour cette approche spécifique. Je ne serais pas surpris si la prise en charge venait dans 2 ou 3 itérations d'iOS à l'avenir. Il suffirait de modifier le protocole pour prendre en charge la population sans initialisation, ou la conformité de base avec les interfaces d'initialisation actuelles de CoreData et la génération de code d'énumération Codable pour les modèles CoreData (qui ont déjà une génération de code). Ces approches ne s'excluent pas mutuellement et 99 % des applications utilisant des données de base mettent en correspondance JSON.

7voto

Schemetrical Points 4820

Swift 4.2 :

Suivant la solution de Casademora,

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

devrait être

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

Cela évite les erreurs que Xcode reconnaît faussement comme des problèmes de découpage de tableau.

Edit : Utiliser des optionnels implicitement déballés pour supprimer le besoin de forcer le déballage. .context à chaque fois qu'il est utilisé.

0 votes

Je préférerais que la constante statique (.context) soit forcée de se déballer lors de la définition plutôt que de la saupoudrer dans tout le code source comme ceci.

0 votes

@casademora c'est la même chose que votre réponse, mais pour swift 4.2 (EDIT : Je vois ce que vous voulez dire, les options implicitement déballées :-). ).

2 votes

Oui, je suis conscient de la différence. Je suggère simplement de mettre le déballage sur la constante (à un endroit) plutôt que sur l'accesseur userInfo (potentiellement partout).

5voto

ChrisH Points 2361

Une alternative pour ceux qui souhaitent utiliser l'approche moderne de XCode en matière de gestion de l'environnement. NSManagedObject j'ai créé un fichier DecoderWrapper pour exposer une classe Decoder que j'utilise ensuite à l'intérieur de mon objet qui se conforme à un JSONDecoding protocole :

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)
    }
}

Ceci n'est probablement utile que pour les modèles sans attributs non-optionnels, mais cela résout mon problème de vouloir utiliser Decodable mais aussi gérer les relations et la persistance avec Core Data sans avoir à créer manuellement toutes mes classes / propriétés.

Editar: Exemple d'utilisation

Si j'ai un objet json :

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

Je crée l'objet dans Core Data (force cast ici pour la brièveté) :

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

Et j'utilise l'extension pour que l'objet décode le json :

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

Maintenant myObject.name es "Something"

0 votes

Si nous avons un objet personnalisé comme @NSManaged public var products : NSSet ? Comment allons-nous décoder cet objet.

0 votes

Vous pouvez le transformer en un ensemble régulier qui est codable.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X