Cannot decode object of class
Asked Answered
R

5

30


I am trying to send a "Class" to my Watchkit extension but I get this error.

* Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '* -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyApp.Person)

Archiving and unarchiving works fine on the iOS App but not while communicating with the watchkit extension. What's wrong?

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")
    }
}
Radiograph answered 6/4, 2015 at 14:8 Comment(1)
you should consider my answer as other way to fix the problem.Abroms
U
17

NOTE: While the information in this answer is correct, the way better answer is the one below by @agy.

This is caused by the compiler creating MyApp.Person & MyAppWatchKitExtension.Person from the same class. It's usually caused by sharing the same class across two targets instead of creating a framework to share it.

Two fixes:

The proper fix is to extract Person into a framework. Both the main app & watchkit extension should use the framework and will be using the same *.Person class.

The workaround is to serialize your class into a Foundation object (like NSDictionary) before you save & pass it. The NSDictionary will be code & decodable across both the app and extension. A good way to do this is to implement the RawRepresentable protocol on Person instead.

Undeceive answered 6/4, 2015 at 14:13 Comment(3)
Thanks. The shared framework fixed it. This was my next step anyway.Radiograph
Might also want to check out github.com/nicklockwood/AutoCoding, it saved me a lot of work !Abduct
The answer below is way better!Undeceive
A
66

According to Interacting with Objective-C APIs:

When you use the @objc(name) attribute on a Swift class, the class is made available in Objective-C without any namespacing. As a result, this attribute can also be useful when you migrate an archivable Objective-C class to Swift. Because archived objects store the name of their class in the archive, you should use the @objc(name) attribute to specify the same name as your Objective-C class so that older archives can be unarchived by your new Swift class.

By adding the annotation @objc(name), namespacing is ignored even if we are just working with Swift. Let's demonstrate. Imagine target A defines three classes:

@objc(Adam)
class Adam:NSObject {
}

@objc class Bob:NSObject {
}

class Carol:NSObject {
}

If target B calls these classes:

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

The output will be:

Adam
B.Bob
B.Carol

However if target A calls these classes the result will be:

Adam
A.Bob
A.Carol

To resolve your issue, just add the @objc(name) directive:

@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")
    }
}
Abroms answered 9/7, 2015 at 13:58 Comment(5)
@bolnad, please find the explained addedAbroms
Was experiencing the archiving issue after adding a new target and this answer solved my problem - thank you!Calpe
thanks. this one is solved my issue clear as glass :)Discordancy
For those who have unarchiving issue when migrating objects from a module to another module, so not OBJ-C to Swift, but Swift module to another Swift module, check my answer: https://mcmap.net/q/390280/-cannot-decode-object-of-classLegislator
Man of the week :)Excruciating
D
35

I had to add the following lines after setting up the framework to make the NSKeyedUnarchiver work properly.

Before unarchiving:

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

Before archiving:

NSKeyedArchiver.setClassName("YourClassName", forClass: YourClassName.self)
Distortion answered 1/6, 2015 at 21:5 Comment(2)
This will not work for nested objects, check my answer: https://mcmap.net/q/390280/-cannot-decode-object-of-classLegislator
For some reason I can't just put the class name, I also have to put the target. So for the class name I have to put "TargetName.ClassName" I don't know if it's because I have multiple targets in my project or what.Cush
U
17

NOTE: While the information in this answer is correct, the way better answer is the one below by @agy.

This is caused by the compiler creating MyApp.Person & MyAppWatchKitExtension.Person from the same class. It's usually caused by sharing the same class across two targets instead of creating a framework to share it.

Two fixes:

The proper fix is to extract Person into a framework. Both the main app & watchkit extension should use the framework and will be using the same *.Person class.

The workaround is to serialize your class into a Foundation object (like NSDictionary) before you save & pass it. The NSDictionary will be code & decodable across both the app and extension. A good way to do this is to implement the RawRepresentable protocol on Person instead.

Undeceive answered 6/4, 2015 at 14:13 Comment(3)
Thanks. The shared framework fixed it. This was my next step anyway.Radiograph
Might also want to check out github.com/nicklockwood/AutoCoding, it saved me a lot of work !Abduct
The answer below is way better!Undeceive
L
17

I had a similar situation where my app used my Core framework in which I kept all model classes. E.g. I stored and retrieved UserProfile object using NSKeyedArchiver and NSKeyedUnarchiver, when I decided to move all my classes to MyApp NSKeyedUnarchiver started throwing errors because the stored objects were like Core.UserProfile and not MyApp.UserProfile as expected by the unarchiver. How I solved it was to create a subclass of NSKeyedUnarchiver and override classforClassName function:

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

Then added @objc(name) to classes which needed to be archived, as suggested in one of the answers here.

And call it like this:

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

It worked very well.

The reason why the solution NSKeyedUnarchiver.setClass(YourClassName.self, forClassName: "YourClassName") was not for me because it doesn't work for nested objects such as when UserProfile has a var address: Address. Unarchiver will succeed with the UserProfile but will fail when it goes a level deeper to Address.

And the reason why the @objc(name) solution alone didn't do it for me was because I didn't move from OBJ-C to Swift, so the issue was not UserProfile -> MyApp.UserProfile but instead Core.UserProfile -> MyApp.UserProfile.

Legislator answered 19/10, 2017 at 14:55 Comment(2)
Excellent answer. The problem is the name of the class. None of the other options had worked for me. Thank you!Whalen
This solution also worked for me where we were forced to rename a module our Core Data stack lived in with the same nested object (only for Transformable type) as described above. Thanks!Disini
A
0

I started facing this after the App Name change,

The error I got was - ".....cannot decode object of class (MyOldModuleName.MyClassWhichISerialized) for key....."

This is because code by default saves Archived object with ModuleName prefix, which will not be locatable after ModuleName changes. You can identify the old Module Name from the error message class prefix, which here is  "MyOldModuleName". 

I simply used the old names to locate the old Archived objects. So before Unarchieving add line,

NSKeyedUnarchiver.setClass(MyClassWhichISerialized.self, forClassName: "MyOldModuleName.MyClassWhichISerialized")

And before Archieving add line

NSKeyedArchiver.setClassName("MyOldModuleName.MyClassWhichISerialized", for: MyClassWhichISerialized.self)
Assurbanipal answered 7/10, 2020 at 20:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.