save and restore an array of custom objects - objective-c

I have an NSArray of custom objects that I want to save and restore. Can this be done with NSUserDefaults?

You can still use NSUserDefaults if you archive your array into NSData.
For Archiving your array, you can use the following code:
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:myArray] forKey:#"mySavedArray"];
And then for loading the custom objects in the array you can use this code:
NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *savedArray = [currentDefaults objectForKey:#"mySavedArray"];
if (savedArray != nil)
{
NSArray *oldArray = [NSKeyedUnarchiver unarchiveObjectWithData:savedArray];
if (oldArray != nil) {
customObjectArray = [[NSMutableArray alloc] initWithArray:oldArray];
} else {
customObjectArray = [[NSMutableArray alloc] init];
}
}
Make sure you check that the data returned from the user defaults is not nil, because that may crash your app.
The other thing you will need to do is to make your custom object comply to the NSCoder protocol. You could do this using the -(void)encodeWithCoder:(NSCoder *)coder and -(id)initWithCoder:(NSCoder *)coder methods.
EDIT.
Here's an example of what you might put in the -(void)encodeWithCoder:(NSCoder *)coder and -(id)initWithCoder:(NSCoder *)coder methods.
- (void)encodeWithCoder:(NSCoder *)coder;
{
[coder encodeObject:aLabel forKey:#"label"];
[coder encodeInteger:aNumberID forKey:#"numberID"];
}
- (id)initWithCoder:(NSCoder *)coder;
{
self = [[CustomObject alloc] init];
if (self != nil)
{
aLabel = [coder decodeObjectForKey:#"label"];
aNumberID = [coder decodeIntegerForKey:#"numberID"];
}
return self;
}

NSUserDefaults cannot write custom objects to file, only ones it knows about (NSArray, NSDictionary, NSString, NSData, NSNumber, and NSDate). Instead, you should take a look at the Archives and Serializations Programming Guide, as well as the NSCoding Protocol Reference, if you're looking to save and restore custom objects to disk. Implementing the protocol is not terribly difficult, and requires very little work.

Custom objects, no. NSUserDefaults only knows about a few basic types (NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary).
Could you use JSON (http://code.google.com/p/json-framework/) to convert your custom object to a string representation, then save an array of those to Defaults? (Using the setObject:forKey: method).
Otherwise, you could look at using sqlite, NSCoder, or even resort to fopen.

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

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.

How to archive an NSArray of custom objects to file in Objective-C

Can you show me the syntax or any sample programs to archive an NSArray of custom objects in Objective-C?
Check out NSUserDefaults.
For Archiving your array, you can use the following code:
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:myArray] forKey:#"mySavedArray"];
And then for loading the custom objects in the array you can use this code:
NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *savedArray = [currentDefaults objectForKey:#"mySavedArray"];
if (savedArray != nil)
{
NSArray *oldArray = [NSKeyedUnarchiver unarchiveObjectWithData:savedArray];
if (oldArray != nil) {
customObjectArray = [[NSMutableArray alloc] initWithArray:oldSavedArray];
} else {
customObjectArray = [[NSMutableArray alloc] init];
}
}
Make sure you check that the data returned from the user defaults is not nil, because that may crash your app.
The other thing you will need to do is to make your custom object to comply to the NSCoder protocol. You could do this using the -(void)encodeWithCoder:(NSCoder *)coder and -(id)initWithCoder:(NSCoder *)coder methods.
If you want to save to a file (rather than using NSUserDefaults) you can use -initWithContentsOfFile: to load, and -writeToFile:atomically: to save, using NSArrays.
Example:
- (NSArray *)loadMyArray
{
NSArray *arr = [NSArray arrayWithContentsOfFile:
[NSString stringWithFormat:#"%#/myArrayFile", NSHomeDirectory()]];
return arr;
}
// returns success flag
- (BOOL)saveMyArray:(NSArray *)myArray
{
BOOL success = [myArray writeToFile:
[NSString stringWithFormat:#"%#/myArrayFile", NSHomeDirectory()]];
return success;
}
There's a lot of examples on various ways to do this here: http://www.cocoacast.com/?q=node/167

NSKeyedUnarchiver objects getting broken?

I have an array that I'm saving to NSUserDefaults, containing an array of my custom class Assignment, which conforms to the NSCoding protocol. The array saves and loads properly, and I can verify that the retrieved first object of the array is of the class Assignment. The problem happens when I try to access ivars of the Assignment object in the array. It crashes and I get the following error:
*** -[CFString respondsToSelector:]: message sent to deallocated instance 0x3948d60
Here is the code I'm using to save to user defaults. Note that I am also retrieving and checking the saved object for debugging purposes.
-(void)saveToUserDefaults:(NSArray*)myArray
{
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
if (standardUserDefaults) {
[standardUserDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:myArray] forKey:#"Assignments"];
[standardUserDefaults synchronize];
}
NSLog(#"Assignments array saved. (%d assignments in array)",[myArray count]);
NSData *dataCheck = [[NSData alloc] initWithData:[standardUserDefaults objectForKey:#"Assignments"]];
NSArray *arrayCheck = [[NSArray alloc] initWithArray:[NSKeyedUnarchiver unarchiveObjectWithData:dataCheck]];
NSLog(#"Checking saved array (%d assignments in array)",[arrayCheck count]);
if ([[arrayCheck objectAtIndex:0] isKindOfClass:[Assignment class]]) {
NSLog(#"It's of the class Assignment.");
}
Assignment *testAssignment = [[Assignment alloc] initWithAssignment:[arrayCheck objectAtIndex:0]];
NSLog(#"Title: %# Course: %#",[testAssignment title],[testAssignment course]);
}
Everything is fine until I allocate testAssignment, which is where the crash happens. Does anyone have any ideas?
EDIT: Here are my NSCoding methods in the Assignment class:
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:title forKey:#"title"];
[coder encodeObject:course forKey:#"course"];
[coder encodeObject:dueDate forKey:#"dueDate"];
[coder encodeObject:notes forKey:#"notes"];
}
- (id)initWithCoder:(NSCoder *)coder {
self = [[Assignment alloc] init];
if (self != nil)
{
title = [coder decodeObjectForKey:#"title"];
course = [coder decodeObjectForKey:#"course"];
dueDate = [coder decodeObjectForKey:#"dueDate"];
notes = [coder decodeObjectForKey:#"notes"];
}
return self;
}
Answered my own question. In initWithCoder, I needed to retain all of the objects I was decoding:
//Example
title = [[coder decodeObjectForKey:#"title"] retain];
Everything works beautifully now. :)