29
votes


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")
    }
}
5
you should consider my answer as other way to fix the problem.agy

5 Answers

16
votes

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.

62
votes

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")
    }
}
33
votes

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)
16
votes

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.

0
votes

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)