Swift 3 Data not conforming to NSCoding - serialization

I tried posting this on Apple's forums but for some reason it's been waiting for moderator approval for a day.
So I want to give my app's users the ability to change the background color, so I have a default value which can be changed, and any changes to that color will be stored as a UserDefault, so I have a computed propety which upon request either reads or writes the value stored in UserDefaults.standard like this:
var homeColor: UIColor? {
get {
guard let data = Defaults.getValue(for: .HomeColor) as? Data else {
return #colorLiteral(red: 0.4119389951, green: 0.8247622848, blue: 0.9853010774, alpha: 1)
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? UIColor ?? #colorLiteral(red: 0.4119389951, green: 0.8247622848, blue: 0.9853010774, alpha: 1)
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
Defaults.set(value: data, for: .HomeColor)
}
}
BTW Defaults is an enum I created to manage UserDefaults more easily, so please don't confuse them.
I know I should convert an UIColor to Data and back when dealing with UserDefaults so I use NSKeyedArchiver and NSKeyedUnarchiver to do that but I still get an exeption saying:
'NSInvalidArgumentException', reason: '-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x6000002439c0'
I can't tell which object is "instance 0x60000005d250" because the inspector is empty, and the debugging info is useless because it just says that my AppDelegate is responsible for that, instead of the actual function that raises the exception. I traced the exeption "manually" using breakpoints and found out it happens whenever I call NSKeyedArchiver.archiveData(withRootObject:), I suspect then that instance 0x60000005d250 is my newValue which is a UIColor, yet the documentation says that UIColor implements NSSecureCoding, which has an encode(withCoder:) function requirement, but there is no other object that could be because that exception only happens when I call NSKeyedArchiver.archiveData(withRootObject:) and that is the only value I pass to it, if I don't do that I get a different exception (which is expectd since UIColor is not compatible with a plist) but there is no
"unrecognized selector sent to instance"
which is the puzzling part.
I also tested the archiving function itself using a value compatible with plist and no exception was raised, so I guess it is not a bug (or at least not an obvious one) caused by some internal variable of that function.
Update
I kept on tracking and actually found out there's nothing wrong with NSKeyedArchiver.encodeData(withRootObject:) it actually returns a Data object, the actual exception is raised when I call UserDaults.standard.set(value:forKey:)(which is not shown in my code directly). So that is the function sending the wrong selector. NSKeyedArchiver.encodeData(withRootObject:) returns swift's own Data instead of NSData. And I searched the documentation and Data does not conform to NSCoding directly and I guess it does not do so through any another protocol. If that was the case it would still be odd since Data is supposed to bridge directly to NSData, so I am not sure what is happening here.

Related

Why can we access a property of a nil NSObject without an exception?

I have this method that I inserted a object and it updates a view based on that object. However, when this object is nil it doesn't crash even when I am not being defensive.
- (void) updateWithObject:(NSObject *)obj {
// obj is nil so how can I access property with out a exception?
if ([(NSDictionary *)[obj property] valueForKey:#"KEY"]) {
//set object values
} else {
//set object values
}
}
That's just the way Objective-C works. To access properties or to call methods you send a message to an object. To do this the C function objc_msgSend is called with the object itself, the selector and all the other arguments that method takes. This does much more than just calling the method. First it checks if the receiver is nil. If it is it returns immediately with an result of nil (or zero or an empty struct). Otherwise it looks up the appropriate method for the selector and calls that if found. If not it enters the message forwarding machinery which sends this object some more messages to dynamically handle this unknown selector. This is all done without throwing exceptions or otherwise crashing. Only as the last resort forwardInvocation: is called whose default implementation in NSObject throws an exception and thus crashes. Before this the object had plenty chances of handling this message.

NSKeyedArchiver and AttributedString [duplicate]

I am trying to store an NSAttributedString to a Core Data SQL store.
I have the property set as a "transformable", it is optional and it is NOT transient or indexed and the value transformer name is set to default "NSKeyedUnarchiveFromData". In the .xcdatamodel and generated the managed object class which has this in the .h:
#property (nonatomic, retain) id Text; (I have tried changing id to NSAttributedString *Text)
and this in the .m:
#dynamic Text;
I look through and set the ".text" property of my NSManagedObject to the attributed string then when completed I do:
NSError *error = nil;
[managedObjectContext save:&error];
This through is causing this error in the output:
[NSCFType encodeWithCoder:]:
unrecognized selector sent to instance
0xc04edb0 Terminating app due to
uncaught exception
'NSInvalidArgumentException', reason:
'* -[NSCFType encodeWithCoder:]:
unrecognized selector sent to instance
0xc04edb0'
I have checked the class of what I am storing to the property and it is NSAttributedString also I check responsesToSelector #selector(:) and this returns true so very confused as this is contrary to the error message?
Please advise.
Thanks
James
For anyone experiencing this problem I found the easiest solution:
In Core Data add an attribute, let's call it attributedText. Then define its type as Transformable. After you create the .h file, change the data type of attributedText from NSDictionary to NSAttributedString.
Now you can save the NSAttributedString in Core Data with no modification needed.
Recalling it is as easy as going:
myObject.attributedText
which will return your NSAttributedString!
Hope this helps someone.
I was checking the Apple Developer Forums and found a thread almost exactly the same as this question, one person had done this but unfortunately did not share the code. All they said was the following:
"In Core Data i have an transformable in the database and i us my own NSVauleTransformer. This is a subclass of NSValueTransformer and creates an attributed string from the data object and back.
Therefore i created a class called PersistableAttributedString which is NSCoding compliant. This class has a string and an array of attributes and builds the attributed string. I also created a class for the possible text attributes which is NSCoding compliant. Together the string and the attributes are all NSCoding compliant.
The class NSAttributedString is also NSCoding compliant, but the attributes are not, that's the problem."
Hope that might help.
In Xcode 10 with automatic code generation this is a lot simpler than the other suggestions.
Select the name of the Attribute and open the Data Model inspector (Command+Option+3)
Set Attribute Type to Transformable
Set Custom Class to NSAttributedString
And that's it, now you can just save your data in your Swift code as you'd expect, e.g.:
detailItem.content = textView.attributedText
Another idea would be to create a Custom Class of NSAttributedString and somewhere use enumerateAttributesInRange:options:usingBlock: to get all the attributes of the string and then save the NSDictionary with the attributes and ranges in to Core Data aswell as the attributed string stripped of it's attributes.
Then when the string is loaded again you could apply the attributes that are in the dictionary to the attributed string using initWithString:attributes:.
It's the font that's giving you grief - a CTDictionary is toll-free bridged to NSDictionary which implements NSCoding so should encode fine.
You might have to deal with the font yourself :( - here's a sucky way of doing it.
1) Instead of storing the NSAttributedString, break it down and put each of it's components into an array.
2) Go through the array - if you see font ref you must store just the information required to re-create this font - have a look at the CTFontCopyFontDescriptor function and the CTFontDescriptorCopyAttribute function should let you get font attributes as a string. Put all these into a NSDictionary which should store in core data fine.
3) Store this array in core data - hopefully all the items in the array will be NSCoding compliant so you should be fine.
...
To recreate your string, when you load from coredata, if you see an NSDctionary representing font attributes you should be able to re-create the fCTFontDescriptor and from that the font.
Then, put your string back together.
I found a way to save attributed text in Swift 4 that does not use a header file. My Core Data store consists of an entity called "AttrNote" with attributes of "title" and "notes". "title" is of type "String" but "notes" is of type "transformable". Then within the view controller where note entry/editing is done I have the following for the save button:
#IBAction func saveBtn(_ sender: Any) {
var note: AttrNote!
//other code
if let title = titleTextField.text {
note.title = title
}
if let noteText = notesTextView.attributedText {
note.notes = noteText
}
And the function that is called to load the data has the following:
func loadNoteData() {
if let note = noteToEdit {
titleTextField.text = note.title
notesTextView.attributedText = note.notes as! NSAttributedString
}
}
And I have the following in ViewDidLoad that enables the B/I/U formatting options to appear with the selection of text:
notesTextView.allowsEditingTextAttributes=true
I am able to save attributed text and then view/edit it later.
OK... Some kind of break through although not a good one...
If we NSLog the attributed string then we can see in there it has NSFont and NSParagraphStyle in. Although these are NOT NSFont and NSParagraphStyle in the code these are CTFontRef and CT Dictionaries of paragraph styles... These are not NS objects although in NSLog they output as these and therefore guess that is why we can not perform the "encodeWithCoder" selector on the object.
IF in the code we just do "NSFont;" the compiler says "NSFont undeclared" so what can we do as we only have the CT functions?
As my colleague said in the comments above if we set the ".text" property to just "NSAttrinutedString *string = [NSAttributedString alloc] initWithString:#"test"] it saves fine and if we remove all the styling from the one we WANT to save it also works!
THERE MUST BE A WAY OF STORING NSATTRIBUTED STRING WITH STYLING INTO CORE DATA... NO?
Any ideas greatly appreciated.
-JM

NSClassFromString() security concerns

I'm trying to create a factory class structure where my abstract base class, PDObject, instantiates an instance the proper subclass based on information passed to it in an NSDictionary. Here's my init method for PDObject:
- (id)initWithDictionary:(NSDictionary *)dictionary inEnvironment:(PDEnvironment *)environment {
NSString *className = [dictionary objectForKey:#"objectType"];
if (className) {
Class objectClass = NSClassFromString(className);
if ([objectClass isSubclassOfClass:[PDObject class]]) {
self = [[objectClass alloc] initWithDictionary:dictionary inEnvironment:environment];
} else {
NSLog(#"tried to instantiate an object of the wrong object type");
self = nil;
}
} else {
NSLog(#"tried to instantiate an object without an object type");
}
return self;
}
I'm wondering if anyone knows of any security concerns with this pattern. I'm worried that something malicious could be passed in in the dictionary and instantiate something unexpected. I have a check to make sure that it is a proper subclass of PDObject. Is there anything I should be concerned about here, or am I just being paranoid?
It is unlikely to be a security hole, but passing potentially random strings to runtime functions isn't really something the runtime is hardened against. The risk isn't instantiating random classes, but causing the app to potentially crash or execute random code.
In general, I wouldn't go beyond minimal effort. To that ends, I would suggest using NSScanner to scan the class name to see if it has any characters that are obviously out of bounds. I would think scanning for alphanumericCharacterSet would be sufficient.
Dynamism is good and I don't see anything particularly risky here. If you want to avoid crashes, you can check for the particular object a. not being nil (just in case) and b. responding to any selector you want to send it. Also note that whichever kind of protection you use, who wants to mock with your app will always be able to do so using library interposition (meet the infamous DYLD_INSERT_LIBRARIES environment variable) and the Objective-C runtime.

delegate not getting nil

I am working on a project in which I perform lazy loading of images. When the imagedownloader downloads the images,it sends the message to its delegate to handle the image. But when its delegate,which is a view controller, gets deallocated from memory,I dont want imagedownloader class to send messages to its delegate as its already dead. I need to know when can i set delegate of imagedownloader to nil?? My target is set to iOS4.0 so i cant use weak references. And i have many instances of imagedownloader stored in a dictionary ready to sent their delegate the completion message . I have to set delegte of all those stored instances to nil.For now i am doing
'
-(void)viewWillDisappear:(BOOL)animated
{
for(imagedownloader *imagedownloaderObj in dict)
{
imagedownloaderObj.delegate = nil;
}
[super viewWillDisAppear:animated]
}
but it crashes in the loop. Please help anyone...and sorry about my bad english but i hope you got it whats my problem..
You have a problem in your code - you are enumerating a dictionary which enumerates its keys, not its objects. Instead you want to do:
for(ImageDownloader *imageDownloader in [imageDownloaderDictionary allValues])
{
if (imageDownloader.delegate == self)
imageDownloader.delegate = nil;
} //note - I've adjusted naming to match Objective-C style conventions. It fits in better with the framework code now.
Also, I'd say to do this in dealloc instead. I don't know that you'll always get a viewWillDisappear: method before deallocating, on earlier version of iOS (including iOS4) you certainly couldn't guarantee that. And furthermore you don't want to waste time downloading the images again if you come back to that view.

NSInvalidArgumentException... how do I define the arguments correctly?

I'm getting this exception on the following code. I think it's because I have not defined the two incoming parameter types. They are local; so how do I define them (and where).
Error: 2011-04-27 11:18:03.226
PointPeek[174:707] * Terminating app
due to uncaught exception
'NSInvalidArgumentException', reason:
'+[SQLiteDB addRecordToDatabase::]:
unrecognized selector sent to class
0x1fe70'
Here's the calling line of code:
[SQLiteDB addRecordToDatabase:
symbol.data: symbol.typeName];
and here's the method I'm calling:
- (void) addRecordToDatabase:data: typeName {
NSString *insertCommand = [NSString stringWithFormat:#"INSERT INTO CardData (CARD_ID, CARD_NAME, CODE_VAL) VALUES ('/%#', '/%#', '/%#')", data, #"Test Card", typeName];
if(sqlite3_open_v2(cDatabasePath, &db, SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK) {
}
Error: 2011-04-27 11:18:03.226
PointPeek[174:707] * Terminating app
due to uncaught exception
'NSInvalidArgumentException', reason:
'+[SQLiteDB addRecordToDatabase::]:
unrecognized selector sent to class
0x1fe70'
Basically, the "unrecognized selector sent to..." message means you tried to tell an object (or class) to do something it doesn't know how to do. ("selector" is another name for method).
You defined your method of the SQLiteDB class as an instance method:
- (void) addRecordToDatabase:data: typeName;
We know that because of the - in the method name (see Methods and Messaging and Class Interface). In the error message you got, notice that it began with a +, which means you attempted to call a method on the SQLiteDB class itself, rather than on an instance of that class.
In other words, you attempted to do this:
[SQLiteDB addRecordToDatabase: symbol.data: symbol.typeName];
when you needed to do something like this:
SQLiteDB *db = [[[SQLiteDB alloc] init] autorelease]; // an instance
[db addRecordToDatabase: symbol.data: symbol.typeName];
(Note that the previous 2 lines of code aren't all that useful in and of themselves. Presumably, instead of creating an instance of SQLiteDB in this method, you'd have it as an instance variable).
[SQLiteDB addRecordToDatabase: symbol.data: symbol.typeName];
That'd assume that addRecordToDabase:: is a class method, not an instance method.
Furthermore, that is an awful name for a method. Try something like:
- (void)addRecordWithData:(NSData*)aData andType:(NSString*)aType;
That is, bare :s are to be avoided and you should always specify the type of the parameter (and not fall back to id as you did here).
Finally, why aren't you using Core Data or, at the very least, FMDB? Raw SQLite is a waste of time.
SQLite is harder to write code for than Core Data, most likely. If you are a newbie to both, Core Data is a better return on investment of your time.
In any case, the questions in your comment indicate that you really need to start by understanding Objective-C. Apple provides an excellent language guide.