Save array of objects with properties to plist - objective-c

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.

Related

Serialization Objective C not working

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.

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).

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.

NSCoding protocol question

I want to add the archiving (NSCoding) protocol to my model class, and then i implement both methods encodeWithCoder:(NSCoder*)coder and initWithCoder:(NSCoder*)coder. MyModelClass has 2 instance variables (NSString and NSImage), so i use the encodeObject:(id)object forKey:(NSString*)string method to encode the object plus the value for particular key. But i keep got the error :
*** -encodeObject:forKey: only defined for abstract class. Define -[NSArchiver encodeObject:forKey:]!
here's my code for NSCoding methods :
- (id)initWithCoder:(NSCoder *)coder {
[super init];
mainPath = [[coder decodeObjectForKey:#"mainPath"] retain];
icon = [[coder decodeObjectForKey:#"icon"] retain];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
NSLog(#"encode with coder is called");
[coder encodeObject:mainPath forKey:#"mainPath"];
[coder encodeObject:icon forKey:#"icon"];
}
And this is how i call them at my controller class :
id object = [assetArray objectAtIndex: [[rows lastObject] intValue]];
if ([object isKindOfClass:[ItemAssetModel class]])
NSLog(#"object is correct");
else
return NO;
NSData *data = [NSArchiver archivedDataWithRootObject: object];
if i change the encodeObject:(id)obj forKey:(NSString*)str with encodeObject:(id)obj, the error is stops, but the result is, the archived data does not copy the instance variable value (cmiiw). Do i miss something on this?
thanks.
hebbian
Try using NSKeyedArchiver instead of NSArchiver.

NSKeyedArchiver archivedDataWithRootObject:

Is the parameter for NSKeyedArchiver archivedDataWithRootObject: supposed to be the array I am trying to save, or the array converted into NSData?
Yuji's answer is right. but more accurately, your element of an array have to implement protocol and fillin your own code to methods initWithCoder: and encodeWithCoder:
like:
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.title = [decoder decodeObjectForKey:#"title"];
self.author = [decoder decodeObjectForKey:#"author"];
self.published = [decoder decodeBoolForKey:#"published"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:title forKey:#"time"];
[encoder encodeObject:author forKey:#"author"];
[encoder encodeBool:published forKey:#"published"];
}
then you can use the archiver and unchariver like:
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:notes];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"notes"];
NSData *notesData = [[NSUserDefaults standardUserDefaults] objectForKey:#"notes"];
NSArray *notes = [NSKeyedUnarchiver unarchiveObjectWithData:notesData];
For more, you can get reference "Archiving Objective-C Objects with NSCoding".
To convert a generic array to an NSData, you need an archiver! If you know how to feed the NSData, you know how to use NSKeyedArchiver. So:
NSArray* array= ... ;
NSData* data=[NSKeyedArchiver archivedDataWithRootObject:array];
Of course all elements in your array needs to implement encodeWithCoder:.