web-dev-qa-db-fra.com

Impossible de décoder l'objet de la classe


J'essaie d'envoyer une "classe" à mon extension Watchkit mais j'obtiens cette erreur.

* Arrêt de l'application en raison d'une exception non interceptée 'NSInvalidUnarchiveOperationException', raison: '* - [NSKeyedUnarchiver decodeObjectForKey:]: ne peut pas décoder l'objet de la classe (MyApp.Person)

L'archivage et le désarchivage fonctionnent correctement sur l'application iOS, mais pas lors de la communication avec l'extension watchkit. Qu'est-ce qui ne va pas?

InterfaceController.Swift

    let userInfo = ["method":"getData"]

    WKInterfaceController.openParentApplication(userInfo,
        reply: { (userInfo:[NSObject : AnyObject]!, error: NSError!) -> Void in

            println(userInfo["data"]) // prints <62706c69 7374303...

            if let data = userInfo["data"] as? NSData {
                if let person = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? Person {
                    println(person.name)
                }
            }

    })

AppDelegate.Swift

func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,
    reply: (([NSObject : AnyObject]!) -> Void)!) {

        var bob = Person()
        bob.name = "Bob"
        bob.age = 25

        reply(["data" : NSKeyedArchiver.archivedDataWithRootObject(bob)])
        return
}

Person.Swift

class Person : NSObject, NSCoding {
    var name: String!
    var age: Int!

    // MARK: NSCoding

    required convenience init(coder decoder: NSCoder) {
        self.init()
        self.name = decoder.decodeObjectForKey("name") as! String?
        self.age = decoder.decodeIntegerForKey("age")
    }

    func encodeWithCoder(coder: NSCoder) {
        coder.encodeObject(self.name, forKey: "name")
        coder.encodeInt(Int32(self.age), forKey: "age")
    }
}
27
fabian

REMARQUE: Bien que les informations de cette réponse soient correctes, la meilleure réponse est celle ci-dessous par @agy.

Cela est dû au fait que le compilateur crée MyApp.Person & MyAppWatchKitExtension.Person de la même classe. Cela est généralement dû au partage de la même classe sur deux cibles au lieu de créer un cadre pour le partager.

Deux correctifs:

La bonne solution consiste à extraire Person dans un framework. L'extension principale de l'application et du watchkit doit utiliser le framework et utilisera le même *.Person classe.

La solution consiste à sérialiser votre classe en un objet Foundation (comme NSDictionary) avant de l'enregistrer et de le transmettre. NSDictionary sera codé et décodable à la fois dans l'application et dans l'extension. Une bonne façon de procéder consiste à implémenter le protocole RawRepresentable sur Person à la place.

15
Jack

Selon Interagir avec les API Objective-C :

Lorsque vous utilisez l'attribut @objc() Sur une classe Swift, la classe est disponible dans Objective-C sans aucun espace de noms. Par conséquent, cet attribut peut également être utile lorsque vous migrez une classe Objective-C archivable vers Swift. Étant donné que les objets archivés stockent le nom de leur classe dans l'archive, vous devez utilisez l'attribut @objc() pour spécifier le même nom que votre classe Objective-C afin que les anciennes archives puissent être désarchivé par votre nouvelle classe Swift.

En ajoutant l'annotation @objc(name), l'espace de noms est ignoré même si nous travaillons simplement avec Swift. Démontrons. Imaginez que la cible A définit trois classes:

@objc(Adam)
class Adam:NSObject {
}

@objc class Bob:NSObject {
}

class Carol:NSObject {
}

Si la cible B appelle ces classes:

print("\(Adam().classForCoder)")
print("\(Bob().classForCoder)")
print("\(Carol().classForCoder)")

La sortie sera:

Adam
B.Bob
B.Carol

Cependant, si la cible A appelle ces classes, le résultat sera:

Adam
A.Bob
A.Carol

Pour résoudre votre problème, ajoutez simplement la directive @objc (nom):

@objc(Person)
class Person : NSObject, NSCoding {
    var name: String!
    var age: Int!

    // MARK: NSCoding

    required convenience init(coder decoder: NSCoder) {
        self.init()
        self.name = decoder.decodeObjectForKey("name") as! String?
        self.age = decoder.decodeIntegerForKey("age")
    }

    func encodeWithCoder(coder: NSCoder) {
        coder.encodeObject(self.name, forKey: "name")
        coder.encodeInt(Int32(self.age), forKey: "age")
    }
}
55
agy

J'ai dû ajouter les lignes suivantes après avoir configuré le framework pour que le NSKeyedUnarchiver fonctionne correctement.

Avant de désarchiver:

NSKeyedUnarchiver.setClass(YourClassName.self, forClassName: "YourClassName")

Avant l'archivage:

NSKeyedArchiver.setClassName("YourClassName", forClass: YourClassName.self)
28
Xishan

J'ai eu une situation similaire où mon application a utilisé mon framework Core dans lequel j'ai conservé toutes les classes de modèle. Par exemple. J'ai stocké et récupéré l'objet UserProfile en utilisant NSKeyedArchiver et NSKeyedUnarchiver, lorsque j'ai décidé de déplacer toutes mes classes vers MyAppNSKeyedUnarchiver j'ai commencé à lancer des erreurs car les objets stockés étaient comme Core.UserProfile et non MyApp.UserProfile comme prévu par le désarchiveur. Comment je l'ai résolu était de créer une sous-classe de NSKeyedUnarchiver et de remplacer la fonction classforClassName:

class SKKeyedUnarchiver: NSKeyedUnarchiver {
    override open func `class`(forClassName codedName: String) -> Swift.AnyClass? {
        let lagacyModuleString = "Core."
        if let range = codedName.range(of: lagacyModuleString), range.lowerBound.encodedOffset == 0  {
            return NSClassFromString(codedName.replacingOccurrences(of: lagacyModuleString, with: ""))
        }
        return NSClassFromString(codedName)
    }
}

Ensuite, nous avons ajouté @objc(name) aux classes qui devaient être archivées, comme suggéré dans l'une des réponses ici.

Et appelez-le comme ceci:

if let unarchivedObject = SKKeyedUnarchiver.unarchiveObject(withFile: UserProfileServiceImplementation.archiveURL.path) as? UserProfile {
    currentUserProfile = unarchivedObject
}

Cela a très bien fonctionné.

La raison pour laquelle la solution NSKeyedUnarchiver.setClass(YourClassName.self, forClassName: "YourClassName") n'était pas pour moi car elle ne fonctionne pas pour les objets imbriqués comme lorsque UserProfile a un var address: Address. Unarchiver réussira avec le UserProfile mais échouera quand il va un niveau plus profond à Address.

Et la raison pour laquelle la solution @objc(name) seule ne l'a pas fait pour moi est que je ne suis pas passé de OBJ-C à Swift, donc le problème n'était pas UserProfile -> MyApp.UserProfile Mais à la place Core.UserProfile -> MyApp.UserProfile.

9
Au Ris