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")
}
}
According to Interacting with Objective-C APIs:
By adding the annotation
@objc(name)
, namespacing is ignored even if we are just working with Swift. Let's demonstrate. Imagine targetA
defines three classes:If target B calls these classes:
The output will be:
However if target A calls these classes the result will be:
To resolve your issue, just add the @objc(name) directive:
I had a similar situation where my app used my
Core
framework in which I kept all model classes. E.g. I stored and retrievedUserProfile
object usingNSKeyedArchiver
andNSKeyedUnarchiver
, when I decided to move all my classes toMyApp
NSKeyedUnarchiver
started throwing errors because the stored objects were likeCore.UserProfile
and notMyApp.UserProfile
as expected by the unarchiver. How I solved it was to create a subclass ofNSKeyedUnarchiver
and overrideclassforClassName
function:Then added
@objc(name)
to classes which needed to be archived, as suggested in one of the answers here.And call it like this:
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 whenUserProfile
has avar address: Address
. Unarchiver will succeed with theUserProfile
but will fail when it goes a level deeper toAddress
.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 notUserProfile
->MyApp.UserProfile
but insteadCore.UserProfile
->MyApp.UserProfile
.I had to add the following lines after setting up the framework to make the
NSKeyedUnarchiver
work properly.Before unarchiving:
Before archiving:
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. TheNSDictionary
will be code & decodable across both the app and extension. A good way to do this is to implement theRawRepresentable
protocol onPerson
instead.