Getting a crash from NSKeyedArchiver
2016-10-06 17:06:06.713 MessagesExtension[24473:2175316] *** NSForwarding:
warning: object 0x61800009d740 of class '' does not implement
methodSignatureForSelector: -- trouble ahead
Unrecognized selector -[MessagesExtension.Model replacementObjectForKeyedArchiver:]
I have created a protocol called Coding that's entire purpose is to simplify NSCoding and NSKeyedArchiver without the need for using pieces of Objective-C.
protocol Coding {
static var directory: FileManager.SearchPathDirectory { get }
static var domainMask: FileManager.SearchPathDomainMask { get }
func encode() -> [String: AnyObject]
init()
init?(dict: [String: AnyObject]?)
}
extension Coding {
static var directory: FileManager.SearchPathDirectory {
return .documentDirectory
}
static var domainMask: FileManager.SearchPathDomainMask {
return .userDomainMask
}
static var directoryURL: String? {
return NSSearchPathForDirectoriesInDomains(Self.directory, Self.domainMask, true).last?.appending("/")
}
func save(to path: String) -> Bool {
guard let url = Self.directoryURL else { return false }
return NSKeyedArchiver.archiveRootObject(self.encode() as NSDictionary, toFile: url + path)
}
static func create(from path: String) -> Self {
guard let url = Self.directoryURL,
let dict = NSKeyedUnarchiver.unarchiveObject(withFile: url + path) as? [String: AnyObject] else { return self.init() }
return self.init(dict: dict) ?? self.init()
}
}
This protocol and extension is suppose to simplify NSCoding and allow for the protocol to be used on Struts. Yet, I am running into the crash above when attempting to save the object.
More specifically, I am getting that crash on the return line of
func save(to path: String) -> Bool {
guard let url = Self.directoryURL else { return false }
return NSKeyedArchiver.archiveRootObject(self.encode() as NSDictionary, toFile: url + path)
}
I have a feeling it has something to do with NSDictionary but I am unsure how to proceed.
Any suggestions??
The Foundation archiving system (NSCoding and related types) was designed for and implemented in Objective-C a long time ago (parts of it are over 20 years old), and expects "objects" to be instances of NSObject. It is simply not realistic to try to use it to encode object graphs containing non-NSObject-like objects. You may well be able to simplify its use in Swift, but you're going to need to make sure everything that the archiver thinks is an NSObject implements the necessary parts of the NSObject API. Since there is no documentation of which parts are used by the archiver, the only sane choice is to subclass NSObject.
I could be wrong of course, but you didn't show us the relevant parts of the code (specifically the implementations of encode).
Vishal S has put together a very nice article on the different ways one can (currently) save data in a structure.
Archiving and Unarchiving Swift Structure Instances
or as Vishal put it:
Swift introduced a tremendous anmount of type-safety. But, archiving and unarchiving always loses the types of the objects. Until a better way comes along to do this by supporting all of Swift’s principles, we should make do with what we have.
Of course one might suggest that the Apple Gurus put a little effort into developing a structure (& type) friendly means of saving your data (!).
Related
Currently I have been working on a task of converting code from objective c to swift. The work was going smooth until I occured with a common resuable code that works in objective c but I haven't getting any idea how should I do that in swift.
The scenario working in objective c is.
I have a common function in my dataManager class
- (void)saveRequest:(id)request forId:(NSNumber *)requestId {
WebRequest *requestData = [[WebRequest alloc] initWithEntity:[NSEntityDescription entityForName:WEB_REQUEST inManagedObjectContext:self.context] insertIntoManagedObjectContext:self.context];
requestData.data = [request toJSON];
requestData.requestId = requestId;
requestData.timestamp = [NSDate date];
[self save];
}
in my project the request classes are already created which contains the toJSON function.
from my controller according to user changes I created the request object and passes the request object to this function and this function calls the toJSON function in the request class and everything works in objective c.
But when I convert this function in swift then it didn't support id as function input variable and if I use Any in place of id then it gives an error that Any don't have any toJSON function.
As this function is common different request objects will come from different controllers.
I don't have any idea how should I go further from hear, If anyone have any idea please help me out
Your class should be like
class WebRequest:NSObject
{
var data :Data?
var requestId: NSNumber?
var timestamp: Date?
init(entity:String , insertIntoManagedObjectContext:NSManagedObjectContext)
{
//your code here
}
}
and your code will be as follows
func saveRequest(request:Request, requestId:NSNumber)
{
let requestData = WebRequest(entity: "entityName", insertIntoManagedObjectContext:self.context)
requestData.data = request.toJSON();
requestData.requestId = requestId;
requestData.timestamp = Date()
}
and Request class in which toJson() present
class Request: NSObject
{
//has some members
func toJSON()->Data
{
return Data()
}
}
There is an existing Swift protocol, Codable (or you can do just Encodable if you want, as Codable is merely Encodable and Decodable), which is designed explicitly for representing an object in JSON (or other formats).
You then use JSONEncoder (rather than JSONSerialization, for example) to encode the object into JSON. See Encoding and Decoding Custom Types:
Consider a Landmark structure that stores the name and founding year of a landmark:
struct Landmark {
var name: String
var foundingYear: Int
}
Adding Codable to the inheritance list for Landmark triggers an automatic conformance that satisfies all of the protocol requirements from Encodable and Decodable:
struct Landmark: Codable {
var name: String
var foundingYear: Int
}
You can then do:
let landmark = Landmark(name: "Big Ben", foundingYear: 1859)
do {
let data = try JSONEncoder().encode(landmark)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
That will product JSON like so:
{
"name": "Big Ben",
"foundingYear": 1859
}
See that Encoding and Decoding Custom Types for more information.
But, if you make your types Codable/Encodable, you could then retire your toJSON method entirely. There’s no need to write code to encode JSON anymore.
If you’re looking for a more tactical edit to your project as you convert it from Objective-C to Swift, you could define your own protocol, say JsonRepresentable, that has a single method requirement, your toJSON (or to whatever you’ve renamed this method during your conversion process).
protocol JsonRepresentable {
func toJSON() -> Data
}
And then, for all of the types that have implemented this method, just add this conformance.
Ideally, go back to those individual files and move the method into an extension for that protocol, e.g., for your first object type:
extension RequestObject1: JsonRepresentable {
func toJSON() -> Data {
...
}
}
And for your second:
extension RequestObject2: JsonRepresentable {
func toJSON() -> Data {
...
}
}
Etc.
is not there a simpler way rather than changing it in whole project
I would suggest that the above is best, but, if you don’t want to go back to all of those individual type declarations, you can just add conformance with an empty extension right where you defined JsonRepresentable:
extension RequestObject1: JsonRepresentable { }
extension RequestObject2: JsonRepresentable { }
As long as those types have implemented that method, these extensions will let the compiler know about their conformance to your protocol.
Anyway, this method can then use this protocol:
func save(_ request: JsonRepresentable, requestId: Int) {
let requestData = ...
requestData.data = request.toJSON()
requestData.requestId = requestId
requestData.timestamp = Date()
save()
}
In past projects, I've had an object use constructor injection for the objects it needs to get some other information. For example:
class Foo {
let appInfo: AppInfoType
init(appInfo: AppInfoType) {
self.appInfo = appInfo
}
}
protocol AppInfoType {
func build(bundle: Bundle) -> String?
}
And then if within Foo, information about the app like build is needed, it can use AppInfoType to get that info. I thought I would see what this looked like with protocol extensions.
extension AppInfoType {
func build(bundle: Bundle) -> String? {
return bundle.infoDictionary?[kCFBundleVersionKey as String] as? String
}
}
class Foo: AppInfoType {
}
So now I can achieve the same thing within Foo by just calling build(bundle: Bundle.main). But is there any easy way to test this now? With the first way, I could still create a MockAppInfoType and provide an implementation for build(bundle: Bundle), but now I don't really see a way to do this unless the protocol extension maybe depended on another protocol where I could inject a mock for that protocol.
I think part of my problem is because Swift 4 has changed the way things like #objc work.
There are a lot of tutorials floating around, with a lot of different values, and I can't pick my way between what used to work in what version enough to figure out how to make it work in this version.
let delegate = UIApplication.shared.delegate as! AppDelegate
delegate.addObserver(self, forKeyPath: #keyPath(AppDelegate.session), options: [], context: nil)
// Warning: Argument of #keyPath refers to non-'#objc' property 'session'
Adding #objc to the var declaration just informs me that APISession can't be referenced in Objective-C. That seems to lead down the path towards requiring me to expose every class / variable I want to use this tool with to Obj-C, and that just seems backwards -- this is a newer feature, as I understand it, and it's just odd that Apple wouldn't make it work natively in Swift. Which, to me, suggests I'm misunderstanding or misapplying something, somewhere, somehow.
According to the docs:
In Objective-C, a key is a string that identifies a specific property of an object. A key path is a string of dot-separated keys that specifies a sequence of object properties to traverse.
Significantly, the discussion of #keyPath is found in a section titled "Interacting with Objective-C APIs". KVO and KVC are Objective-C features.
All the examples in the docs show Swift classes which inherit from NSObject.
Finally, when you type #keyPath in Xcode, the autocomplete tells you it is expecting an #objc property sequence.
Expressions entered using #keyPath will be checked by the compiler (good!), but this doesn't remove the dependency on Objective-C.
This is how I've applied #keyPath() in real project of mine. I used it to save & retrieve data to and from UserDefaults and I called that feature as AppSettings. Here's how things are going on...
1). I have a protocol called AppSettingsConfigurable It contains a couple of stuffs which are the setting features of my app...
//: AppSetting Protocol
#objc protocol AppSettingsConfigurable {
static var rememberMeEnabled : Bool { get set }
static var notificationEnabled : Bool { get set }
static var biometricEnabled : Bool { get set }
static var uiColor: UIColor? { get set }
}
2). I have class and I named it AppSettings. This is where saving and retrieving operation take place with UserDefaults
//: AppSettings
class AppSettings: NSObject {
fileprivate static func updateDefaults(for key: String, value: Any) {
// Save value into UserDefaults
UserDefaults.standard.set(value, forKey: key)
}
fileprivate static func value<T>(for key:String) -> T? {
// Get value from UserDefaults
return UserDefaults.standard.value(forKey: key) as? T
}
}
3). Here's where BIG things are happened. Conform AppSettings class to our protocol and lets implement the stuffs using #keyPath().
//: Conform to protocol
extension AppSettings:AppSettingsConfigurable{
/** get & return remember me state */
static var rememberMeEnabled: Bool {
get { return AppSettings.value(for: #keyPath(rememberMeEnabled)) ?? false }
set { AppSettings.updateDefaults(for: #keyPath(rememberMeEnabled), value: newValue) }
}
/** get & return notification state */
static var notificationEnabled: Bool {
get { return AppSettings.value(for: #keyPath(notificationEnabled)) ?? true }
set { AppSettings.updateDefaults(for: #keyPath(notificationEnabled), value: newValue) }
}
/** get & return biometric state */
static var biometricEnabled: Bool {
get { return AppSettings.value(for: #keyPath(biometricEnabled)) ?? false}
set { AppSettings.updateDefaults(for: #keyPath(biometricEnabled), value: newValue) }
}
/** get & return biometric state */
static var uiColor: UIColor? {
get { return AppSettings.value(for: #keyPath(uiColor)) }
set { AppSettings.updateDefaults(for: #keyPath(uiColor), value: newValue!) }
}
}
PS: Noticed something different with uiColor from the rest? Nothing wrong with it as it's optional and it's allowed to accept the nil
Usage:
//: Saving...
AppSettings.biometricEnabled = true
//: Retrieving...
let biometricState = AppSettings.biometricEnabled // true
I'm using a library called Gloss to help parse JSON data. As a result I've created structs that are of type Glossy:
struct LoyaltyCard: Glossy {
let id: Int
init?(json: JSON) {
guard let __id: Int = "id" <~~ json
else { return nil }
}
I have many different Glossy structs and want to pass them into a function along with a string but I keep getting an error: " Cannot invoke 'getMemberInfo' with an argument list of type '(String, memberData: LoyaltyCard.Type)'", here is an abbreviated version of my function:
func getMemberInfo<T: Glossy> (memberDataRequest: String, memberData:T) {
let urlAccess = "\(baseURL)/api/\(memberDataRequest)"
///////code////////////
let data = object as! NSData
let jsonInfo: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.init(rawValue: 0))
let jsonArray = jsonInfo as! NSArray
if let dict = jsonArray[0] as? JSON //This means as type Gloss
{
let loyaltyID= LoyaltyCard(json: dict)
print(loyaltyID?.id)
}
}
Any ideas how to make this function work?
I'm inferring from your code sample and from your comments, that you don't necessarily want to pass a Glossy type to getMemberInfo, but rather that the key requirement is that you want to perform a network request and return a Glossy type.
While I get what you were trying to do, I would personally retire the generic approach, and just use a protocol extension. You end up with a method that can be called for any Glossy type. And if this protocol extension method returns a type Self, that will end up returning whatever Glossy type from which you happen to call it.
First, let's step back and be clear as to what the Glossy protocol might look like. At the very least, you'd have some failable initializer (plus whatever else your types needed):
protocol Glossy {
init?(json: [String: AnyObject])
}
(Note, I'm not using JSON type, but feel free if you want. I personally just use Swift collections for parsed JSON, but do whatever you want.)
I'd then define a static method in a protocol extension to perform the request. The following method uses NSURLSession, but if you use Alamofire or something else, the basic idea is the same:
extension Glossy {
static func performMemberRequest(memberDataRequest: String, completionHandler:(Self?, ErrorType?) -> ()) -> NSURLSessionTask {
let urlAccess = "\(baseURL)/api/\(memberDataRequest)"
let request = NSMutableURLRequest(URL: NSURL(string: urlAccess)!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard let data = data where error == nil else {
completionHandler(nil, error)
return
}
do {
if let array = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [[String: AnyObject]],
let dictionary = array.first {
completionHandler(Self(json: dictionary), nil)
} else {
completionHandler(nil, GlossyError.InvalidJSONError)
}
} catch let parseError {
completionHandler(nil, parseError)
}
}
task.resume()
return task
}
}
Note, there are a few notable issues entailed in the above:
Network requests should always be performed asynchronously. So use asynchronous pattern like completionHandler rather than trying to return some object immediately.
If you're going to return anything, the only thing you should be returning is the NSURLSessionTask object so the caller has the option to capture that and cancel requests, should you want that functionality.
I changed the name of the method to be more descriptive and conform to Cocoa naming conventions.
As an aside, your code suggests that your API returned an array and you're just grabbing the first dictionary. That seems like a curious pattern, but I've followed that, above. If you really were returning an array, it strikes me that you did that because you contemplate a scenario where you could be returning multiple items. In that case, I would advise iterate through the whole array and have the completion handler return [Self]? (an array of Glossy objects) rather than just Self? (i.e. only the first one).
Furthermore, I wouldn't personally advise a structure that returns an array as the top level item. How does that web service report errors? I'd have a dictionary structure that returned success/failure and/or return code and the like. And then have a dedicated key for results which would be your array of results.
But I didn't tackle any of these broader API issues above, but rather followed the pattern in your code snippet. But these are considerations you might want to think about.
In my example, I didn't dispatch these completionHandler calls back to the main queue, but that's often a very useful pattern (avoids synchronization problems, UI updates, etc.). It's trivial to do, but I wanted to keep the above relatively simple.
But, let's step aside from the details of your API and the like. Let's focus on the notion that you want a static method defined in the protocol extension, (and it can therefore be called from any type that conforms to Glossy). For example, I can then define the LoyaltyCard class with the required initializer:
struct LoyaltyCard: Glossy {
let id: Int
init?(json: [String: AnyObject]) {
guard let id = json["id"] as? Int else {
return nil
}
self.id = id
}
}
Having done all that, I can now invoke the static method of Glossy protocol extension on LoyaltyCard, for example:
LoyaltyCard.performMemberRequest(memberDataRequest) { loyaltyCard, error in
guard let loyaltyCard = loyaltyCard where error == nil else {
print(error)
return
}
// do something with loyaltyCard here
print(loyaltyCard)
}
// but don't use it here
There's a lot there, but I don't want you to get lost in the details. But I do hope you grok the key concepts here: Don't pass a Glossy type to your method, nor use a generic: Instead use protocol extension. And avoid synchronous network requests, so instead use a asynchronous pattern like the completionHandler pattern.
NSKeyedUnarchiver.decodeObject will cause a crash / SIGABRT if the original class is unknown. The only solution I have seen to catching this issue dates from Swift's early history and required using Objective C (also pre-dated Swift 2's implementation of guard, throws, try & catch). I could figure out the Objective C route - but I would prefer to understand a Swift-only solution if possible.
For example - the data has been encoded with NSPropertyListFormat.XMLFormat_v1_0. The following code will fail at unarchiver.decodeObject() if the class of the encoded data is unknown.
//...
let dat = NSData(contentsOfURL: url)!
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat)
//it will crash after this if the class in the xml file is not known
if let newListCollection = (unarchiver.decodeObject()) as? List {
return newListCollection
} else {
return nil
}
//...
I am looking for a Swift 2 only way to test whether the data is valid before attempting .decodeObject - since .decodeObject has no throws - which means that try - catch does not seem to be an option in Swift (methods without throws cannot be wrapped AFAIK). Or else an alternative way of decoding the data which will throw an error I can catch if the decode fails. I want the user to be able to import a file from iCloud drive or Dropbox - therefore it needs to be properly validated. I cannot assume that the encoded data is safe.
The NSKeyedUnarchiver methods .unarchiveTopLevelObjectWithData & .validateValue both have throws. Is there perhaps some way that these could be used? I cannot work out how to even begin to attempt to implement validateValue in this context. Is this even a possible route? Or should I be looking to one of the other methods for a solution?
Or does anyone know an alternative Swift 2 only way of addressing this issue? I believe that the key I am interested in is probably entitled $classname - but TBH I am out of my depth with respect to trying to work out how to implement validateValue - or even whether that would be the correct route to persevere with. I have the sense that I am missing something obvious.
EDIT: Here is a solution - thanks to rintaro's great answer(s) below
The initial answer solved the issue for me - i.e. implementing a delegate.
For now however I have gone with a solution built around rintaro's additional edited response as follows:
//...
let dat = NSData(contentsOfURL: url)!
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat)
do {
let decodedDataObject = try unarchiver.decodeTopLevelObject()
if let newListCollection = decodedDataObject as? List {
return newListCollection
} else {
return nil
}
}
catch {
return nil
}
//...
When NSKeyedUnarchiver encounters unknown classes, unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:) delegate method is called.
The delegate may, for example, load some code to introduce the class to the runtime and return the class, or substitute a different class object. If the delegate returns nil, unarchiving aborts and the method raises an NSInvalidUnarchiveOperationException.
So, you can implement the delegate like this:
class MyUnArchiverDelegate: NSObject, NSKeyedUnarchiverDelegate {
// This class is placeholder for unknown classes.
// It will eventually be `nil` when decoded.
final class Unknown: NSObject, NSCoding {
init?(coder aDecoder: NSCoder) { super.init(); return nil }
func encodeWithCoder(aCoder: NSCoder) {}
}
func unarchiver(unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? {
return Unknown.self
}
}
Then:
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat)
let delegate = MyUnArchiverDelegate()
unarchiver.delegate = delegate
unarchiver.decodeObjectForKey("root")
// -> `nil` if the root object is unknown class.
ADDED:
I didn't noticed that NSCoder has extension with more swifty methods:
extension NSCoder {
#warn_unused_result
public func decodeObjectOfClass<DecodedObjectType : NSCoding where DecodedObjectType : NSObject>(cls: DecodedObjectType.Type, forKey key: String) -> DecodedObjectType?
#warn_unused_result
#nonobjc public func decodeObjectOfClasses(classes: NSSet?, forKey key: String) -> AnyObject?
#warn_unused_result
public func decodeTopLevelObject() throws -> AnyObject?
#warn_unused_result
public func decodeTopLevelObjectForKey(key: String) throws -> AnyObject?
#warn_unused_result
public func decodeTopLevelObjectOfClass<DecodedObjectType : NSCoding where DecodedObjectType : NSObject>(cls: DecodedObjectType.Type, forKey key: String) throws -> DecodedObjectType?
#warn_unused_result
public func decodeTopLevelObjectOfClasses(classes: NSSet?, forKey key: String) throws -> AnyObject?
}
You can:
do {
try unarchiver.decodeTopLevelObjectForKey("root")
// OR `unarchiver.decodeTopLevelObject()` depends on how you archived.
}
catch let (err) {
print(err)
}
// -> emits something like:
// Error Domain=NSCocoaErrorDomain Code=4864 "*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyProject.MyClass) for key (root); the class may be defined in source code or a library that is not linked" UserInfo={NSDebugDescription=*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyProject.MyClass) for key (root); the class may be defined in source code or a library that is not linked}
another way is to fix the name of the class used for NSCoding. You simply have to use:
NSKeyedArchiver.setClassName("List", forClass: List.self before serializing
NSKeyedUnarchiver.setClass(List.self, forClassName: "List") before deserializing
wherever needed.
Looks like iOS extensions prefix the class name with the extension's name.
Actually, it's the reason which we should dig deeply matters. There's a possible, you create a archive path named xxx.archive, then you unarchive from the path(xxx.archive), now everything is ok. But if change target name, when you unarchive, the crash occurred!!! It's because archive&unarchive the different object(the truth is we archive&unarchive target.obj, not just the obj).
so simple way is to delete the archive path or just use a different archive path. And then we should consider how avoid the crash, try-catch is our helper mentioned by rintaro.
I was having same issue. Adding #objc to class declaration worked for me.
#objc(YourClass)
class YourClassName: NSObject {
}