Serialization Objective C not working - objective-c

I'm trying to make an app with Objective C.
I'm trying to serialise an array existing out of objects and after wards deserialise it. Inside the object there are the methods
(id)initWithCoder:(NSCoder *)coder` and `encodeWithCoder:(NSCoder *)coder
But it seems the "rootObject" stays "nil" in the "loadDataFromDisk" -method
Here is my code :
#import "Alarm.h"
#implementation Alarm
#synthesize array = _array;
#synthesize time = _time;
#synthesize coder = _coder;
-(id)initWithCoder:(NSCoder *)coder
{
self = [super init];
if(self)
{
_array = [coder decodeObjectForKey:#"array"];
_time = [coder decodeObjectForKey:#"time"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:self.array forKey:#"array"];
[coder encodeObject:self.time forKey:#"time"];
}
#end
My save and load methods :
-(void)saveDataToDisk
{
NSString * path = [self pathForDataFile];
NSLog(#"Writing alarms to '%#' %lu", path, (unsigned long)array.count);
NSMutableDictionary * rootObject;
rootObject = [NSMutableDictionary dictionary];
[rootObject setValue:array forKey:#"alarms"];
[NSKeyedArchiver archiveRootObject:rootObject toFile:path];
}
-(void)loadDataFromDisk
{
NSString *path = [self pathForDataFile];
NSDictionary *rootObject = [NSMutableDictionary dictionary];
rootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
// "array" is an array with Objects of "Alarm"
array = [rootObject valueForKey:#"alarms"];
NSLog(#"Loaded from : %# %lu",path ,(unsigned long)array.count);
}
I hope anyone can help me out with this.
Thanks in advance.

You #synthized the array backing store (ivar) as _array. So you need to access the array as either _array or self.array. In saveDataToDisk and loadDataFromDisk it is accessed as array.
To test your array coding try something simple like this:
NSLog(#"array: %#", self.array);
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.array];
NSLog(#"data: %#", data);
NSArray *recovered = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSLog(#"recovered: %#", recovered);
Note: There is no need to wrap your array in a NSMutableDictionary.
When that works change it to the file based method calls.
Check that the filePath is valid.
Check that the file is created.
Check that the file contents are the same as in the above test code.
Note: There is no reason to wrap your array in a NSMutableDictionary.

Related

Save array of objects with properties to plist

I have a Mutable Array containing different objects such as strings, UIImage , etc.
They are sorted like this:
Example:
BugData *bug1 = [[BugData alloc]initWithTitle:#"Spider" rank:#"123" thumbImage:[UIImage imageNamed:#"1.jpeg"]];
...
...
NSMutableArray *bugs = [NSMutableArray arrayWithObjects:bug1,bug2,bug3,bug4, nil];
So basically it's an array with objects with different properties.
I Tried to save a single string to a file with the next code and it's working fine but when I try to save the array with the objects, i get an empty plist file.
NSString *docsDir = [NSHomeDirectory() stringByAppendingPathComponent:#"Documents"];
NSString * path = [docsDir stringByAppendingPathComponent:#"data.plist"];
NSLog(#"%#",bugs); //Making sure the array is full
[bugs writeToFile:path atomically:YES];
What am I doing wrong?
When you write a string or any primitive data to plist it can be saved directly. But when you try to save an object, you need to use NSCoding.
You have to implement two methods encodeWithCoder: to write and initWithCoder: to read it back in your BugData Class.
EDIT:
Something like this :
Change Float to Integer or String or Array as per your requirement and give a suitable key to them.
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:_title forKey:#"title"];
[coder encodeFloat:_rating forKey:#"rating"];
NSData *image = UIImagePNGRepresentation(_thumbImage);
[coder encodeObject:(image) forKey:#"thumbImage"];
}
- (id)initWithCoder:(NSCoder *)coder {
_title = [coder decodeObjectForKey:#"title"];
_rating = [coder decodeFloatForKey:#"rating"];
NSData *image = [coder decodeObjectForKey:#"thumbImage"];
_thumbImage = [UIImage imageWithData:image];
return self;
}
Even this will help you.
Implement NSCoding in your BugData class as below
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeFloat:title forKey:#"title"];
[coder encodeFloat:rank forKey:#"rank"];
[coder encodeObject:UIImagePNGRepresentation(thumbImage) forKey:#"thumbImageData"];
}
- (id)initWithCoder:(NSCoder *)coder {
title = [coder decodeFloatForKey:#"title"];
rank = [coder decodeFloatForKey:#"rank"];
NSData *imgData = [coder decodeObjectForKey:#"thumbImageData"];
thumbImage = [UIImage imageWithData:imgData ];
return self;
}
BugData must implement the NSCoding protocol.You need this method to encode the data:
- (void) encodeWithCoder: (NSCoder*) encoder;
Where you should provide a NSData object representing the class (decode it with the decoder).
To read the plist you need to implement this method:
-(id) initWithCoder: (NSCoder*) decoder;
Where you read data from decoder and return a BugData object.

How can I save an Objective-C object that's not a property list object or is there a better way for this than a property list?

I'm writing a Cookbook application, and I've not been able to find anything on how to save the data of a class I've created (the Recipe class). The only way I've seen would be to possibly save the contents of this class as a whole without individually saving every element of the class for each object by making this method for my Recipe class:
-(void) writeToFile:(NSString *)file atomically:(BOOL)atomic{
}
But I have absolutely no idea how I'd go about implementing this to save this object to a file using this method. Some of the properties are:
NSString* name;
UIImage* recipePicture;
NSDate* dateAdded;
NSMutableArray* ingredients; //The contents are all NSStrings.
Does anyone know how to go about saving an object of the Recipe class?
It's been driving me crazy not being able to figure it out. Any help would be greatly appreciated. I already have a .plist entitled "RecipeData.plist".
Would I just need to write every property to the plist and initialize a new object of recipe with those properties at run time?
Adopt:
#interface Recipe : NSObject<NSCoding>
Implement:
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:name_ forKey:#"name"];
[coder encodeObject:recipePicture_ forKey:#"recipePicture"];
[coder encodeObject:dateAdded_ forKey:#"dateAdded"];
[coder encodeObject:ingredients_ forKey:#"ingredients"];
}
// Decode an object from an archive
- (id)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self!=NULL)
{
name_ = [coder decodeObjectForKey:#"name"];
recipePicture_ = [coder decodeObjectForKey:#"recipePicture"];
dateAdded_ = [coder decodeObjectForKey:#"dateAdded"];
ingredients_ = [coder decodeObjectForKey:#"ingredients"];
}
return self;
}
Now in your save:
- (void) save:(NSString*)path recipe:(Recipe*)recipe
{
NSMutableData* data=[[NSMutableData alloc] init];
if (data)
{
NSKeyedArchiver* archiver=[[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
if (archiver)
{
[archiver encodeInt:1 forKey:#"Version"];
[archiver encodeObject:recipe forKey:#"Recipe"];
[archiver finishEncoding];
[data writeToFile:path atomically:YES];
}
}
}
And in the load:
- (Recipe*) load:(NSString*)path
{
Recipe* ret=NULL;
NSData* data=[NSData dataWithContentsOfFile:path];
if (data)
{
NSKeyedUnarchiver* unarchiver=[[NSKeyedUnarchiver alloc] initForReadingWithData:data];
if (unarchiver)
{
int version=[unarchiver decodeIntForKey:#"Version"];
if (version==1)
{
ret=(Recipe*)[unarchiver decodeObjectForKey:#"Recipe"];
}
[unarchiver finishDecoding];
}
}
return ret;
}
One option (besides encoding/decoding) is to store each attribute of your class in a dictionary. Then you write the dictionary to the file. The trick is to ensure that every object you put in the dictionary is allowed in a plist. Of the four properties you show, all but the UIImage can be stored as-is.
-(BOOL)writeToFile:(NSString *)file atomically:(BOOL)atomic{
NSMutableDictionary *data = [NSMutableDictionary dictionary];
[data setObject:name forKey:#"name"];
[data setObject:dateAdded forKey#"dataAdded"];
NSDate *imageData = UIImagePNGRepresentation(recipePicture);
[data setObject:imageData forKey:#"recipePicture"];
// add the rest
return [data writeToFile:file atomically:YES];
}
I updated this to return a BOOL. If it fails, it means one of two things:
The file was inappropriate
You tried to save a non-plist friendly object in the dictonary
You need to add code to avoid trying to add nil objects if you have any. The important thing is to ensure that all keys are strings and only plist-friendly objects are stored (NSString, NSNumber, NSDate, NSValue, NSData, NSArray, and NSDictionary).

Can't get NSData into NSMutableArray

I have an array of objects that I want to save as a file and reload back into my app. It's saving the file (with some data inside) but I can't get it to read back into a NSMutable Array.
The objects are models that conform to the NSCoding protocol:
#implementation myModel
#synthesize name;
#synthesize number;
-(void) encodeWithCoder: (NSCoder *) encoder
{
[encoder encodeObject:name forKey:#"name"];
[encoder encodeInteger:number forKey:#"number"];
}
-(id) initWithCoder: (NSCoder *) decoder
{
name = [decoder decodeObjectForKey:#"name"];
number = [decoder decodeIntegerForKey:#"number"];
return self;
}
#end
So I create an array of these objects, then I save it...
- (void) saveMyOptions {
// Figure out where we're going to save the app's data files
NSString *directoryPath = [NSString stringWithFormat:#"%#/Library/Application Support/MyAppDir/", NSHomeDirectory()]; // points to application data folder for user
// Figure out if that directory exists or not
BOOL isDir;
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager fileExistsAtPath:directoryPath isDirectory:&isDir];
// If the directory doesn't exist, create it
if (!isDir)
{
[fileManager createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// Assemble everything into an array of objects with options
NSMutableArray *savedPreferences = [[NSMutableArray alloc] init];
myModel *saveOptions = nil;
for (int i; i < [otherArray count]; i++)
{
saveOptions = [[myModel alloc] init];
[saveOptions setName:#"Some String"];
[saveOptions setNumber:i];
[savedPreferences addObject:saveOptions];
saveOptions = nil;
}
// Actually save those options into a file
NSData* saveData = [NSKeyedArchiver archivedDataWithRootObject:savedPreferences];
NSString *fileName = [NSString stringWithFormat:#"%#filename.stuff", directoryPath];
NSError *error = nil;
BOOL written = [saveData writeToFile:fileName options:0 error:&error];
if (!written)
{
NSLog(#"Error writing file: %#", [error localizedDescription]);
}
}
So now I try to load that data back into an array. This is where I think it's falling apart...
- (NSMutableArray *) loadOptions {
// Create file manager object
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSData *saveData = nil;
// Find user directory path
NSString *directoryPath = [NSString stringWithFormat:#"%#/Library/Application Support/MyAppDir/", NSHomeDirectory()]; // points to application data folder for user
// Assign file name
NSString *fileName = [NSString stringWithFormat:#"%#filename.stuff", directoryPath];
// Create options array
NSMutableArray *myOptions = nil;
// If the file exists, fill the array with options
if ([fileManager fileExistsAtPath:fileName])
{
saveData = [NSData dataWithContentsOfFile:fileName];
myOptions = [NSKeyedUnarchiver unarchiveObjectWithData:saveData];
}
NSLog(#"%lu", [myOptions count]); // This ALWAYS reports 0!
NSLog(#"%lu", [saveData length]); // This reports a value of 236;
return myOptions;
}
Could someone point me in the direction of where I'm going wrong? I'm throughly confused :-(
Thanks in advance!
You are missing the super calls in your encodeWithCoder: and initWithCoder: methods, but that's just a guess. Why not use NSUserDefaults for saving preferences?
You might also want to make sure that your objects are retained is set using the synthesized setter.
- (void)encodeWithCoder:(NSCoder *)encoder {
[super encodeWithCoder:encoder];
[encoder encodeObject:name forKey:#"name"];
[encoder encodeInteger:number forKey:#"number"];
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (self) {
self.name = [decoder decodeObjectForKey:#"name"];
self.number = [decoder decodeIntegerForKey:#"number"];
}
return self;
}
For your info, NSKeyedArchiver also has a method you can use directly to operate on files:
+ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path
and NSKeyedUnarchiver:
+ (id)unarchiveObjectWithFile:(NSString *)path

Write custom object to .plist in Cocoa

I am blocking into something and I am sure it is too big.
I have a custom object that look like this
#interface DownloadObject : NSObject <NSCoding>{
NSNumber *key;
NSString *name;
NSNumber *progress;
NSNumber *progressBytes;
NSNumber *size;
NSString *path;
}
#property (copy) NSNumber *key;
#property (copy) NSString *name;
#property (copy) NSNumber *progress;
#property (copy) NSNumber *size;
#property (copy) NSString *path;
#property (copy) NSNumber *progressBytes;
-(id)initWithKey:(NSNumber *)k name:(NSString *)n progress:(NSNumber *)pro size:(NSNumber *)s path:(NSString *)p progressBytes:(NSNumber *)pb;
#end
And the implementation
#implementation DownloadObject
#synthesize size, progress, name, key, path, progressBytes;
-(id)initWithKey:(NSNumber *)k name:(NSString *)n progress:(NSNumber *)pro size:(NSNumber *)s path:(NSString *)p progressBytes:(NSNumber *)pb
{
self.key = k;
self.name = n;
self.progress = pro;
self.size = s;
self.path = p;
self.progressBytes = pb;
return self;
}
-(id) initWithCoder: (NSCoder*) coder {
if (self = [super init]) {
self.key = [[coder decodeObjectForKey:#"Key"] retain];
self.name = [[coder decodeObjectForKey:#"Name"] retain];
self.progress = [[coder decodeObjectForKey:#"Progress"] retain];
self.size = [[coder decodeObjectForKey:#"Size"] retain];
self.path = [[coder decodeObjectForKey:#"Path"] retain];
self.progressBytes = [[coder decodeObjectForKey:#"ProgressBytes"]retain];
}
return self;
}
-(void) encodeWithCoder: (NSCoder*) coder {
[coder encodeObject:self.key forKey:#"Key"];
[coder encodeObject:self.name forKey:#"Name"];
[coder encodeObject:self.progress forKey:#"Progress"];
[coder encodeObject:self.size forKey:#"Size"];
[coder encodeObject:self.path forKey:#"Path"];
[coder encodeObject:self.progressBytes forKey:#"ProgressBytes"];
}
-(void)dealloc
{
[key release];
[name release];
[size release];
[progress release];
[path release];
[progressBytes release];
[super dealloc];
}
#end
As you can see it implement NSCoding (I think so, NSObject does not conform to NSCoding). Now when I try to do something like that just to test
downloadArray = [[[NSMutableArray alloc]init]retain];
NSNumber *number = [NSNumber numberWithInt:10];
DownloadObject *object = [[DownloadObject alloc]initWithKey:number name:#"hey" progress:number size:number path:#"hey" progressBytes:number];
[downloadArray addObject:object];
[object release];
[downloadArray writeToFile:path atomically:YES];
downloadArray is a NSMutableArray. My plist read/write is fine, the path is located in the application support and when I log it show the plist path.
But it just does not write the array to the plist, any idea ?
Property list files can only store basic data types and cannot contain custom objects. You need to convert your object to an NSData object if you want it to be written to the plist. You can do this with NSKeyedArchiver, which will encode an object which conforms to the NSCoding protocol into an NSData object.
DownloadObject *object = [[DownloadObject alloc]initWithKey:number name:#"hey" progress:number size:number path:#"hey" progressBytes:number];
NSData* objData = [NSKeyedArchiver archivedDataWithRootObject:object];
[downloadArray addObject:objData];
[object release];
When you want to reconstruct your object from the NSData object, you use NSKeyedUnarchiver:
NSData* objData = [downloadArray objectAtIndex:0];
DownloadObject* object = [NSKeyedUnarchiver unarchiveObjectWithData:objData];
You also have several memory leaks in your code. In your -initWithCoder: method, you should not be using accessors to set the value of the ivars, you should just set the ivars directly, like so:
key = [[coder decodeObjectForKey:#"Key"] copy];
You are calling -retain and then using the accessor which is specified as copy, which will mean your object has a retain count of 2 and will not be released. In general you should avoid using accessors in init methods.
Also, in the code where you allocate your downloadArray object, you are calling -alloc and then -retain on the object, which will leave it with a retainCount of 2. You should re-read the Objective-C Memory Management Guidelines.
This works for me:
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:highScoreArray forKey:kHighScoreArrayKey];
[archiver finishEncoding];
[data writeToFile:[self dataFilePath] atomically:YES];
[data release];
[archiver release];
BOOL flag = false;
ObjectFileClass *obj = [yourMutableArray objectAtIndex:0];
//TO Write Data . . .
NSData* archiveData = [NSKeyedArchiver archivedDataWithRootObject:obj.title];
flag =[archiveData writeToFile:path options:NSDataWritingAtomic error:&error];
}
if (flag) {
NSLog(#"Written");
//To Read Data . . .
NSData *archiveData = [NSData dataWithContentsOfFile:path];
id yourClassInstance = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData]; // choose the type of your class instance . . .
NSLog(#"%#",yourClassInstance);
}else{
NSLog(#"Not Written");
}

Storing Custom Object in NSMutableDictionary

I am trying to store a custom object in NSMutableDictionary. After saving when I read the object from NSMutableDictionary it's always null.
Here is the code
//Saving
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
CustomObject *obj1 = [[CustomObject alloc] init];
obj1.property1 = #"My First Property";
[dict setObject:obj1 forKey:#"FirstObjectKey"];
[dict writeToFile:[self dataFilePath] atomically:YES];
// Reading
NSString *filePath = [self dataFilePath];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithContentsOfFile:filePath];
CustomObject *tempObj = [dict objectForKey:#"FirstObjectKey"];
NSLog(#"Object %#", tempObj);
NSLog(#"property1:%#,,tempObj.property1);
How can I store a custom class object in NSMutableDictionary?
The problem is not with putting the object into the dictionary; the problem is with writing it to a file.
Your custom class has to be serializable. You need to implement the NSCoding protocol so that Cocoa knows what to do with your class when you ask for it to be written out to disk.
This is pretty simple to do; you need to implement two methods that will look something like the following:
- (id)initWithCoder:(NSCoder *)coder {
self = [super init];
// If inheriting from a class that implements initWithCoder:
// self = [super initWithCoder:coder];
myFirstIvar = [[coder decodeObjectForKey:#"myFirstIvar] retain];
mySecondIvar = [[coder decodeObjectForKey:#"mySecondIvar] retain];
// etc.
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
// If inheriting from a class that implements encodeWithCoder:
// [super encodeWithCoder:coder];
[coder encodeObject:myFirstIvar forKey:#"myFirstIvar"];
[coder encodeObject:mySecondIvar forKey:#"mySecondIvar"];
// etc.
}
Essentially you're just listing the ivars that you need to save, and then reading them back in properly.
UPDATE: As mentioned by Eimantas, you'll also need NSKeyedArchiver. To save:
NSData * myData = [NSKeyedArchiver archivedDataWithRootObject:myDict];
BOOL result = [myData writeToFile:[self dataFilePath] atomically:YES];
To reload:
NSData * myData = [NSData dataWithContentsOfFile:[self dataFilePath]];
NSDictionary * myDict = [NSKeyedUnarchiver unarchiveObjectWithData:myData];
I think that should do it.
writeToFile method can store only standard types of objects into plist. If you have custom object you'd have to use NSKeyedArchiver/NSKeyedUnarchiver for this.