Write Object to Plist using NSCoding and NSCoder - ios

I've created a class that conforms to NSCoding
- (id) initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
self.name = [aDecoder decodeObjectForKey:#"name"];
self.type = [aDecoder decodeObjectForKey:#"type"];
}
return self;
}
- (void) encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.name forKey:#"name"];
[aCoder encodeObject:self.type forKey:#"type"];
}
Then the methods used for saving to and reading from the plist are as follows:
- (IBAction)read:(id)sender
{
NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:self.plistDocPath];
//NSLog(#"plist contents: %#",dic);
NSData *dataObj = [dic objectForKey:#"obj2"];
NSLog(#"dataObj: %#",dataObj);
ObjClass *obj = [NSKeyedUnarchiver unarchiveObjectWithData:dataObj];
NSLog(#"decoded object: %#",obj);
}
- (IBAction)write:(id)sender
{
ObjClass *obj = [[ObjClass alloc] initWithName:#"nick" andType:#"human"];
//NSString *str = #"example";
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:self.plistDocPath];
NSString *key = [NSString stringWithFormat:#"obj%i",dic.count];
NSData *objData = [NSKeyedArchiver archivedDataWithRootObject:obj];
[dic setObject:objData forKey:key];
[dic writeToFile:self.plistDocPath atomically:YES];
}
The writing to process seems to be working fine if I look into the file I'm seeing an NSData object for key "obj2" with bytes in it.
The problem is when I try to read from the file I'm getting
2014-02-17 20:48:53.780 FileManagerSample[589:70b] *** NSForwarding: warning: object 0x1661f24 of class 'Object' does not implement methodSignatureForSelector: -- trouble ahead
2014-02-17 20:48:53.780 FileManagerSample[589:70b] *** NSForwarding: warning: object 0x1661f24 of class 'Object' does not implement doesNotRecognizeSelector: -- abort
Can someone please help because I'm stuck big time.

Related

Unarchive custom object with secure coding in iOS

I'm encoding an array of objects that conform to NSSecureCoding successfully like this.
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array requiringSecureCoding:YES error:nil];
How do I unarchive this data? I'm trying like this but it's returning nil.
NSError *error = nil;
NSArray<MyClass *> *array = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSArray class] fromData:data error:&error];
This is the error it returns.
The data couldn’t be read because it isn’t in the correct format.
Here is a simple example
#interface Secure:NSObject<NSSecureCoding> {
NSString* name;
}
#property NSString* name;
#end
#implementation Secure
#synthesize name;
// Confirms to NSSecureCoding
+ (BOOL)supportsSecureCoding {
return true;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeObject:self.name forKey: #"name"];
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
self = [self init];
if (self){
self.name = [coder decodeObjectOfClass:[NSString class] forKey:#"name"];
}
return self;
}
#end
Usage
Secure *archive = [[Secure alloc] init];
archive.name = #"ARC";
NSArray* array = #[archive];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array requiringSecureCoding:YES error:nil];
NSArray<Secure *> *unarchive = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSObject class] fromData:data error:nil];
NSLog(#"%#", unarchive.firstObject.name);

NSCoding data not saved when returning to the app?

I don't understand why previousArrays returns (null), I would like to save a class containing a bezier path and its color.
The code : (after touchesEnded is called, a path is created and saved in memory. When I come back to the app with initWithCoder, previousArrays is (null) ) :
-(id)initWithCoder:(NSCoder *)aDecoder{
if ( !(self=[super initWithCoder:aDecoder])) return nil;
CGContextRef context = UIGraphicsGetCurrentContext();
NSArray *previousArrays = [SaveData loadDict];
NSLog(#"previousArrays : %#", previousArrays);//***HERE*** : return (null)
for ( id object in previousArrays){
//for ( NSDictionary*dict in previousArrays){
NSLog(#"obj %#", [[object class] description]);
//...
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:myPath];//nscoding compliant
DataForPath *firstPath = [[DataForPath alloc] init];
firstPath.path = bezierPath;
firstPath.colorInArray = #(currentColor);
NSLog(#"touchEnded, firstPath : %#", firstPath);
NSDictionary *dict = #{#"firstPath":firstPath};
[SaveData saveDict:dict];
}
#implementation SaveData
static NSString* kMyData = #"data1";
+ (void) saveDict:(NSDictionary*) dict{
NSLog(#"saving data...");
//retrieve previous data
NSData *previousData = [[NSUserDefaults standardUserDefaults] objectForKey:kMyData];
NSMutableArray *previousArray = [[NSKeyedUnarchiver unarchiveObjectWithData:previousData] mutableCopy];
[previousArray addObject:dict];
//add new data, and save
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:previousArray];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:kMyData];
//NSLog(#"%#", [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]);
}
+(NSArray*) loadDict {
NSLog(#"loading data...");
NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:kMyData];
NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithData:data];
return array;
}
#import <Foundation/Foundation.h>
#import UIKit;
#interface DataForPath : NSObject <NSCoding>
#property (nonatomic, strong) UIBezierPath* path;
#property (nonatomic, strong) NSNumber *colorInArray;
#end
#implementation DataForPath
- (id)initWithCoder:(NSCoder *)decoder{
if ( !(self = [super init]) ) return nil;
self.path = [decoder decodeObjectForKey:#"path"];
self.colorInArray = [decoder decodeObjectForKey:#"colorInArray"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder{
[encoder encodeObject:self.path forKey:#"path"];
[encoder encodeObject:self.colorInArray forKey:#"colorInArray"];
}
Also, is there a way to see what is in the NSUserDefault, such as a "prettyjson" format? -dictionaryRepresentation returns some figures like that <124d1f>
Did you checked saveDict method for previousArray == nil ?, Because before you saved something you have nothing in userDefaults
I had to use this : if ( previousArray == nil ) ...
NSMutableArray *previousArray = [[NSKeyedUnarchiver unarchiveObjectWithData:previousData] mutableCopy];
if ( previousArray == nil ) previousArray = [[NSMutableArray alloc] init];
Not enough information.
Objects can be converted to and from data if they conform to the NSCoding protocol. You have to write code for both the encodeWithCoder and initWithCoder methods that saves all your object's properties.
If you are un-archiving an object that you saved with encodeWithCoder and one of it's properties is not loading then there is most likely a problem with your encodeWithCoder method.
Post the header for the class you're trying to encode, all of the encodeWithCoder and initWithCoder methods, and information about the properties of your object that you are encoding.
Also post the code that converts your object to data and saves it, and provide a description of when/how it is saved.

Saving data with NSUserDetaults and load after phone restart wont work

I managed to save an NSMutableArray in NSUserDefaults by first converting it to NSData - I dont deal with a lot of data and just want the data to be there after I switch off & on my phone - but the data does not show up in my table where I would display it. I write the NSUserDefaults back to my array upon loading. Maybe one of you has a hint...? Below the button action where I write to NSUserDefaults and the method viewDidLoad where I write NSUserDefaults to my original array (toDoitems)
- (IBAction)unwindToList:(UIStoryboardSegue *)segue
{
XYZAddToDoItemViewController *source = [segue sourceViewController];
XYZToDoItem *item = source.toDoItem;
if (item !=nil) {
[self.toDoitems addObject:item];
NSString *error;
NSData *data = [NSPropertyListSerialization dataFromPropertyList:self.toDoitems format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"itemArray"];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.tableView reloadData];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.toDoitems = [[NSMutableArray alloc] init];
self.toDoitems = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:#"itemArray"]];
}
Heres one way to do this
Add encoder decoder functions to your XYZToDoItem class
Something like this if say you had 2 strings in this class string1 and string2 :
(i havent compiled this code but you get the idea)
-(void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.string1 forKey:#"string1"];
[aCoder encodeObject:self.string2 forKey:#"string2"];
}
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self)
{
self.string1 = [aDecoder decodeObjectForKey:#"string1"];
self.string2 = [aDecoder decodeObjectForKey:#"string2"];
}
return self;
}
Then when you are ready to save do the following
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.myDataArray];
[userDefaults setObject:data forKey:#"storageName"];
// To load the data from NSUserDefaults
NSData *myData = [[NSUserDefaults standardUserDefaults] objectForKey:#"storageName"];
NSArray *temp = (NSMutableArray*)[NSKeyedUnarchiver unarchiveObjectWithData:myData];
self.myDataArray = (NSMutableArray*)[temp mutableCopy];

Working with NSUserDefaults when saving own datatype

i had read this topic How to save My Data Type in NSUserDefault? and get from there this useful part of code:
MyObject *myObject = [[MyObject alloc] init];
NSData *myObjectData = [NSData dataWithBytes:(void *)&myObject length:sizeof(myObject)];
[[NSUserDefaults standardUserDefaults] setObject:myObjectData forKey:#"kMyObjectData"];
for saving data and this for reading
NSData *getData = [[NSData alloc] initWithData:[[NSUserDefaults standardUserDefaults] objectForKey:#"kMyObjectData"]];
MyObject *getObject;
[getData getBytes:&getObject];
its works very good when i save data in one ViewController and read it in other.
but when i whant to use it in the same class:
- (IBAction)linkedInLog:(UIButton *)sender
{
NSUserDefaults *myDefaults = [[NSUserDefaults standardUserDefaults] objectForKey:#"linkedinfo"];
NSData *getData = [[NSData alloc] initWithData:myDefaults];
LinkedContainer *getObject;
[getData getBytes:&getObject];
if (!myDefaults) {
mLogInView = [[linkedInLoginView alloc]initWithNibName:#"linkedInLogInView" bundle:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(loginViewDidFinish:)
name:#"loginViewDidFinish"
object:mLogInView];
[self.navigationController pushViewController:mLogInView animated:YES];
if ((FBSession.activeSession.isOpen)&&(mLinkedInIsLogegOn)) {
mMergeButton.hidden = NO;
}
}
else{
mLinkedInIsLogegOn= YES;
mLinkedInInfo.mConsumer = getObject.mConsumer;
mLinkedInInfo.mToken = getObject.mToken;
}
}
something going wrong. in #selector:loginViewDidFinish i am saving my data to NSUserDefaults:
-(void) loginViewDidFinish:(NSNotification*)notification
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
mLinkedInInfo.mConsumer = mLogInView.consumer;
mLinkedInInfo.mToken = mLogInView.accessToken;
NSData *myObjectData = [NSData dataWithBytes:(void *)&mLinkedInInfo length:sizeof(mLinkedInInfo)];
NSUserDefaults *lSave = [NSUserDefaults standardUserDefaults];
[lSave setObject:myObjectData forKey:#"linkedinfo"];
[lSave synchronize];
if (mLinkedInInfo.mToken) {
mLinkedInIsLogegOn = YES;
}
}
the program always crashes when it comes to else part. If somebody knows what I am doing wrong please help me)
error message: Thread 1 : EXC_BAD_ACCESS(code=2,address 0x8) when compiling getObject.Consumer
In the vast majority of cases, this is not going to be a meaningful way to serialize your object into an NSData:
MyObject *myObject = [[MyObject alloc] init];
NSData *myObjectData = [NSData dataWithBytes:(void *)&myObject length:sizeof(myObject)];
[[NSUserDefaults standardUserDefaults] setObject:myObjectData forKey:#"kMyObjectData"];
The canonical way to do this would be for MyObject to adopt the NSCoding protocol. Based on the code you posted here, an adoption of NSCoding might look like this:
- (id)initWithCoder:(NSCoder *)coder
{
if (self = [super init])
{
mConsumer = [coder decodeObjectForKey: #"consumer"];
mToken = [coder decodeObjectForKey: #"token"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:mConsumer forKey: #"consumer"];
[coder encodeObject:mToken forKey:#"token"];
}
Once you had done that work, you would convert MyObject to and from NSData like this:
NSData* data = [NSKeyedArchiver archivedDataWithRootObject: myObject];
MyObject* myObject = (MyObject*)[NSKeyedUnarchiver unarchiveObjectWithData: data];
The code you have here is totally going to smash the stack and crash (because this line [getData getBytes:&getObject]; will cause the NSData to write bytes to the address of getObject, which is locally declared on the stack. Hence stack smashing.) Starting from your code, a working implementation might look something like this:
- (IBAction)linkedInLog:(UIButton *)sender
{
NSData* dataFromDefaults = [[NSUserDefaults standardUserDefaults] objectForKey:#"linkedinfo"];
LinkedContainer* getObject = (LinkedContainer*)[NSKeyedUnarchiver unarchiveObjectWithData: dataFromDefaults];
if (!dataFromDefaults) {
mLogInView = [[linkedInLoginView alloc]initWithNibName:#"linkedInLogInView" bundle:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(loginViewDidFinish:)
name:#"loginViewDidFinish"
object:mLogInView];
[self.navigationController pushViewController:mLogInView animated:YES];
if ((FBSession.activeSession.isOpen)&&(mLinkedInIsLogegOn)) {
mMergeButton.hidden = NO;
}
}
else{
mLinkedInIsLogegOn= YES;
mLinkedInInfo.mConsumer = getObject.mConsumer;
mLinkedInInfo.mToken = getObject.mToken;
}
}
-(void) loginViewDidFinish:(NSNotification*)notification
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
mLinkedInInfo.mConsumer = mLogInView.consumer;
mLinkedInInfo.mToken = mLogInView.accessToken;
NSData* objectData = [NSKeyedArchiver archivedDataWithRootObject: mLinkedInInfo];
[[NSUserDefaults standardUserDefaults] setObject: objectData forKey: #"linkedinfo"];
[[NSUserDefaults standardUserDefaults] synchronize];
if (mLinkedInInfo.mToken) {
mLinkedInIsLogegOn = YES;
}
}
I agree with ipmcc 's answer, another viable option would be to add methods to your object to convert it to an NSDictionary. You could add methods to -initWithDictionary as well and should make instantiation very easy. Pull from dictionary in NSUserDefaults to use, convert to dictionary to save.
Here is an example of those 2 methods with generic data:
- (id)initWithDictionary:(NSDictionary *)dict
{
self = [super init];
// This check serves to make sure that a non-NSDictionary object
// passed into the model class doesn't break the parsing.
if(self && [dict isKindOfClass:[NSDictionary class]]) {
NSObject *receivedFences = [dict objectForKey:#"fences"];
NSMutableArray *parsedFences = [NSMutableArray array];
if ([receivedFences isKindOfClass:[NSArray class]]) {
for (NSDictionary *item in (NSArray *)receivedFences) {
if ([item isKindOfClass:[NSDictionary class]]) {
[parsedFences addObject:[Fences modelObjectWithDictionary:item]];
}
}
}
}
// More checks for specific objects here
return self;
}
- (NSDictionary *)dictionaryRepresentation
{
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionary];
NSMutableArray *tempArrayForFences = [NSMutableArray array];
for (NSObject *subArrayObject in self.fences) {
if([subArrayObject respondsToSelector:#selector(dictionaryRepresentation)]) {
// This class is a model object
[tempArrayForFences addObject:[subArrayObject performSelector:#selector(dictionaryRepresentation)]];
} else {
// Generic object
[tempArrayForFences addObject:subArrayObject];
}
}
[mutableDict setValue:[NSArray arrayWithArray:tempArrayForFences] forKey:#"fences"];
return [NSDictionary dictionaryWithDictionary:mutableDict];
}
This is basically boilerplate code that is generated by a program I use called JSON Accelerator. It will read a JSON string returned by an API and generate object code for you. Not really a new concept, but makes created classes for API's very easy. And this bit of code works great for creating dictionaries to be saved to NSUserDefaults. Hope this helps.

NSCoding: Custom Class unarchived, but with empty/nil class members

I want to persist an NSDictionary, filled with custom Object to disc:
NSDictionary *menuList = [[NSMutableDictionary alloc]initWithDictionary:xmlParser.items];
//here the "Menu List"`s Object are filled correctly
//persisting them to disc:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *directory = [paths objectAtIndex:0];
NSString *fileName = [NSString stringWithUTF8String:MENU_LIST_NAME];
NSString *filePath = [directory stringByAppendingPathComponent:fileName];
//saving using NSKeyedArchiver
NSData* archiveData = [NSKeyedArchiver archivedDataWithRootObject:menuList];
[archiveData writeToFile:filePath options:NSDataWritingAtomic error:nil];
//here the NSDictionary has the correct amount of Objects, but the Objects` class members are partially empty or nil
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSDictionary *theMenu = (NSDictionary*)[NSKeyedUnarchiver unarchiveObjectWithData:data];
Here the .m of the Custom Object (the kind of object stored in the NSDictionary)
- (id)initWithTitle:(NSString*)tTitle level:(NSString*)tLevel state:(NSString*)tState visible:(BOOL)tVisible link:(NSString*)tLink linkType:(NSString*)tLinkType anId:(NSString*)tId {
if ((self = [super init])) {
self.anId = tId;
self.title = tTitle;
self.level = tLevel;
self.state = tState;
self.visible = tVisible;
self.link = tLink;
self.linkType = tLinkType;
}
return self;
}
- (void) encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.anId forKey:#"anId"];
[encoder encodeObject:self.level forKey:#"level"];
[encoder encodeObject:self.state forKey:#"state"];
[encoder encodeBool:self.visible forKey:#"visible"];
[encoder encodeObject:self.title forKey:#"title"];
[encoder encodeObject:self.link forKey:#"link"];
[encoder encodeObject:self.linkType forKey:#"linkType"];
}
- (id)initWithCoder:(NSCoder *)decoder {
if(self == [super init]){
self.anId = [decoder decodeObjectForKey:#"anId"];
self.level = [decoder decodeObjectForKey:#"level"];
self.state = [decoder decodeObjectForKey:#"state"];
self.visible = [decoder decodeBoolForKey:#"visible"];
self.title = [decoder decodeObjectForKey:#"title"];
self.link = [decoder decodeObjectForKey:#"link"];
self.linkType = [decoder decodeObjectForKey:#"linkType"];
}
return self;
}
#end
I dont know why the objects are unarchived correctly, but the object´s members are lost somewhere. I assume there must be an error somewhere in the NSCoding methods, but I just can´t find it, any help much appreciated.
When you implement the initWithCoder: method, you need to call super properly:
if (self = [super initWithCoder:decoder]) {
The instance that is being unarchived has more attributes than just the additions in your specific class. You also don't want to be checking equality with self, you want to be assigning to self.

Resources