Storing Custom Object in NSMutableDictionary - objective-c

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.

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

Can't access NSDictionary values

I have an NSDictionary called equiposDic, which I need to retrieve using a value from another NSDictionary (value dip from ) but I can't' access it:
equiposDic = [[NSDictionary alloc] initWithObjectsAndKeys:#"FC Barcelona Regal", #"49", #"Real Madrid", #"50", #"Caja Laboral", #"51", #"Banca CĂ­vica", #"52", #"Gescrap Bizkaia",#"53", #"Valencia Basket", #"13",#"Lucentum Alicante",#"54",#"Lagun Aro GBC",#"4",#"CAI Zaragoza", #"55", #"Assignia Manresa",#"2", #"FIATC Mutua Joventut",#"8",#"Unicaja",#"56",#"Gran Canaria",#"57",#"Mad-Croc Fuenlabrada",#"9",#"Blusens Monbus",#"59",#"UCAM Murcia",#"58", #"Asefa Estudiantes",#"60", #"Blancos de Rueda Valladolid", #"11", nil];
NSDictionary *posicion = [[NSDictionary alloc] initWithDictionary: [ligaArray objectAtIndex:indexPath.row]];
NSString *equipo = [posicion valueForKey:#"idp"];
NSString *idp = [posicion valueForKey:#"idp"];
NSLog(#"equipo %#", [equiposDic objectForKey:#"49"]);
Many thanks
If posicion is a NSDictionary, you have to use objectForKey in order to get the string.
NSString *idp = [posicion objectForKey:#"idp"];
Is ligaArray available in the context ?
I explain, your ligaArray var maynot be available when you expect it. You have to store ligaArray in your class with a retain
.h :
#interface MyClass {
NSMutableArray *ligaArray;
}
.m :
-viewDidLoad{
[super viewDidLoad];
ligaArray = [[NSMutableArray array] retain];
// OR
ligaArray = [[NSMutableArray alloc] init];
}
-dealloc{
[ligaArray release];
}
If this is not the case, try it. If it's already the case, check the retain/release calls.
This way, you will not loose reference to ligaArray values in your tableview methods.

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");
}

Saving data with NSMutableDictionary

I had a method to save a dic to the disk:
+(BOOL) writeApplicationData:(NSDictionary *)data
bwriteFileName:(NSString *)fileName
{
NSLog(#"writeApplicationData");
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
if (!documentsDirectory) {
NSLog(#"Documents directory not found!");
return NO;
}
NSString *appFile = [documentsDirectory stringByAppendingPathComponent:fileName];
return ([data writeToFile:appFile atomically:YES]);
}
And I tested it with:
NSMutableDictionary *dic = [[NSMutableDictionary alloc] init];
NSMutableDictionary *d1 = [[NSMutableDictionary alloc] init];
NSMutableDictionary *d2 = [[NSMutableDictionary alloc] init];
[d1 setObject:#"d11"
forKey:#"d11"];
[d1 setObject:#"d12"
forKey:#"d12"];
[d1 setObject:#"d13"
forKey:#"d13"];
[d2 setObject:#"d21"
forKey:#"d21"];
[d2 setObject:#"d22"
forKey:#"d22"];
[d2 setObject:#"d23"
forKey:#"d23"];
[dic setObject:d1
forKey:#"d1"];
[dic setObject:d2
forKey:#"d2"];
[self writeApplicationData:dic
bwriteFileName:#"testSave"];
And the data is saved correctly.
Then I tried to save d1 with class obj in it:
LevelInfoData *levelInfoData = [[LevelInfoData alloc] init];
[levelInfoDictionary setObject:levelInfoData
forKey:#"test"];
[dic setObject:levelInfoDictionary
forKey:#"LevelInfoDictionary"];
But this time, even no plist file was generated in the disk.
Here is the LevelInfoData class:
#interface LevelInfoData : NSObject {
int levelNum;
}
#property (nonatomic) int levelNum;
#end
#implementation LevelInfoData
#synthesize levelNum;
#synthesize isLevelLocked;
#synthesize isLevelCleared;
#synthesize levelHighScore;
-(id)init
{
if( (self = [super init]) ) {
levelNum = 0;
}
return self;
}
#end
I'm really confused, hope somebody could help me out, thanks.
The contents of the dictionary need to be property list type objects.
From the NSDictionary Class Reference:
This method recursively validates that all the contained objects are property list objects (instances of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary) before writing out the file, and returns NO if all the objects are not property list objects, since the resultant file would not be a valid property list.
https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSDictionary_Class/Reference/Reference.html
You may want to try making your custom class a subclass of NSData rather than NSObject.
I'm not sure how attached you are to NSDictionary, but this may be a situation where NSCoder will better serve you.
See nscoder vs nsdictionary when do you use what
More details here:
NSCoder Class Reference
Some code snippets
A tutorial