可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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
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") } }
回答1:
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.
回答2:
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") } }
回答3:
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)
回答4:
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
.