NSCoder encode a NSDIctionary - objective-c

This question will seem a copy, but it is not.
My Contact class confirms to NSCoding protocol, implementing methods:
#pragma mark Encoding/Decoding
-(void)encodeWithCoder:(NSCoder *)aCoder
{
NSLog(#"Encoding");
[aCoder encodeObject:self.firstName forKey:#"firstName"];
NSLog(#"First name encoded");
[aCoder encodeObject:self.lastName forKey:#"lastName"];
NSLog(#"Last name encoded");
[aCoder encodeInt:self.age forKey:#"age"];
NSLog(#"Age encoded");
[aCoder encodeObject:self.phoneNumbers forKey:#"phoneNumbers"];
NSLog(#"Encoding finished");
}
- (id) initWithCoder: (NSCoder *)coder
{
if (self = [super init])
{
[self setFirstName:[coder decodeObjectForKey:#"firstName"]];
[self setLastName:[coder decodeObjectForKey:#"lastName"]];
[self setPhoneNumbers:[coder decodeObjectForKey:#"phoneNumbers"]];
[self setAge:[coder decodeIntForKey:#"age"]];
}
return self;
}
PhoneNumbers is a dictionary, when encoding reaches to encode it, the application crashes. And this is how I serialize Contacts array:
#pragma mark Import/Export
//Export Contacts to file
-(void)exportContactsToFile
{
BOOL done=[NSKeyedArchiver archiveRootObject:self.contacts toFile:[PathUtility getFilePath:#"phonebook"]];
}
//Import Contacts from file
-(void)importContactsFromFile
{
self.contacts = [NSKeyedUnarchiver unarchiveObjectWithFile:[PathUtility getFilePath:#"phonebook"]];
}
how should I serialize NSDictionary as property?
Thanks

The NSPropertyListSerialization class provides the serialization methods that convert property list objects to and from either an XML or an optimized binary format. The NSPropertyListSerialization class object provides the interface to the serialization process; you don’t create instances of NSPropertyListSerialization.
NSDictionary *propertyList= #{ #"FirstNameKey" : #"Edmund",
#"LastNameKey" : #"Blackadder" };
NSString *errorStr;
NSData *dataRep = [NSPropertyListSerialization dataFromPropertyList:propertyList
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorStr];
if (!dataRep) {
// Handle error
}

Related

iOS NSKeyedUnarchiver calling singleton getter

I have a singleton class that will initialize it's data from a web service then save itself to a file (using NSCoding and NSKeyedUnarchiver) so I can just initialize this file instead of pulling all the data from the web again. Because it's a singleton instance that is being saved, I want to be able to call the singleton getter like you normally would, check to see if there's an archived copy and if not, pull the data down and archive it. Archiving is working fine, but when I try to call [[NSKeyedUnarchiver unarchiveObjectWithFile: filePath] retain] it calls the sharedInstance getter before sharedInstance is initialized. This causes init to be called and the app then downloads all the data again, just to be subsequently overwritten by the unarchiver.
Am I doing something wrong with my setup, or is there another way of Serializing this data?
Here's some (simplified) code:
#implementation Helmets
#synthesize helmetList, helmetListVersion;
//Class Fields
static Helmets *sharedInstance = nil;
// Get the shared instance and create it if necessary.
+ (Helmets *)sharedInstance {
//if(sharedInstance == nil){
//[Helmets unarchive]; //This does not work! Calls sharedInstance() again (recursion)
if(sharedInstance == nil){
sharedInstance = [[super allocWithZone:NULL] init]; //Pull from web service
[Helmets archive]; //Save instance
//}
//}
return sharedInstance;
}
- (id)init {
self = [super init];
if (self) {
helmetList = [[NSMutableArray alloc]init];
//Get our data from the web service and save it (excluded)
}
}
return self;
}
//This works!
+(void)archive{
if([NSKeyedArchiver archiveRootObject:sharedInstance toFile:[Helmets getFilePath]]){
NSLog(#"Archiving Successful");
}
else{
NSLog(#"Archiving Failed");
}
}
//This works, but calls getInstance causing data to be downloaded anyways!
+(void)unarchive{
// Check if the file already exists
NSFileManager *filemgr = [NSFileManager defaultManager];
NSString *filePath = [Helmets getFilePath];
if ([filemgr fileExistsAtPath: filePath])
{
sharedInstance = [[NSKeyedUnarchiver unarchiveObjectWithFile: filePath] retain];
}
[filemgr release];
}
Instance is initialized like:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
...
[Helmets unarchive]; //This calls sharedInstance() too soon!
[Helmets sharedInstance];
}
The class implements NSCoding in the .h and overrides initWithCoder and encodeWithCoder (Archiving is working).
Thanks in advance!
In the end you need a private method to set your shared instance in addition to the one you have, and you need a different init, again private to the implementation.
- (id)initAndFetch:(BOOL)fetch
{
if((self = [super init])) {
...
if(fetch) { do the web fetch };
...
}
}
In the +sharedInstance method, you will pass YES.
Then your decode will look like:
- (id)initWithCoder:(NSCoder *)decoder
{
if((self = [self initAndFetch:NO])) {
title = [decoder decodeObjectForKey:#"title"];
...
sharedInstance = self;
}
return self;
}
I have got to make it work the following manner.
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (!self) {
return nil;
}
UserInfo *user =[UserInfo sharedInstance];
return user;
}
-(void)encodeWithCoder:(NSCoder *)aCoder
{
UserInfo *user =[UserInfo sharedInstance];
[aCoder encodeObject: user.phoneNumber forKey:#"phoneNumber"];
}
-(void)save
{
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:[UserInfo sharedInstance]] forKey:#"USER_DATA"];
[[NSUserDefaults standardUserDefaults]synchronize];
}
-(UserInfo*)currentUser
{
NSData *data =[[NSUserDefaults standardUserDefaults] objectForKey:#"USER_DATA"];
UserInfo *savedUser =[NSKeyedUnarchiver unarchiveObjectWithData:data];
UserInfo *user =[UserInfo sharedInstance];
return user;
}

NSUserDefaults with Custom object that has an array of custom objects

I have an object that I convert into NSData using an NSKeyedArchiver and then store it into NSUserDefaults. Everything gets saved correctly except for the elements of an array that the object has. All the objects in the array have conform to the NSCoder protocols (or whatever theyre called- ex. self.property = [decoder decodeObjectForKey:#"key"] and [encoder encodeObjectForKey:#"key"])
When I save the object, the elements of the array remain in the array, but their properties themselves do not get saved. i DO call the sycnrhonize method, so that is not the issue.
NOTE that all other times i save & load it is correct, it just does not save the elements of an array that belongs to an object. Do i have to save that separately?
The "current status" NSNumber is not being saved. Objective and target are being saved
import "Level.h"
#implementation Level
#synthesize objective = _objective;
#synthesize isComplete = _isComplete;
#synthesize goldReward = _goldReward;
#synthesize xpReward = _xpReward;
#synthesize missionID = _missionID;
#synthesize currentStatus = _currentStatus;
#synthesize targetName = _targetName;
#synthesize owner = _owner;
-(void)dealloc{
[super dealloc];
}
-(id)initWithMissionID:(int)number{
if (self = [super init]) {
self.currentStatus = 0;
self.isComplete = NO;
self.missionID = [NSNumber numberWithInt:number];
[self setUpMisson];
}
return self;
}
-(void)setUpMisson{
if ([self.missionID intValue] == 0) {
self.xpReward = [NSNumber numberWithInt:100];
self.goldReward = [NSNumber numberWithInt:100];
self.objective = [NSNumber numberWithInt:3];
self.targetName = #"Swordsman";
CCLOG(#"Gotta kill some swordsmen!");
}
}
-(void)encodeWithCoder:(NSCoder *)encoder{
[encoder encodeObject:self.objective forKey:#"objective"];
[encoder encodeObject:self.isComplete forKey:#"isComplete"];
[encoder encodeObject:self.goldReward forKey:#"goldReward"];
[encoder encodeObject:self.xpReward forKey:#"xpReward"];
[encoder encodeObject:self.missionID forKey:#"missionID"];
[encoder encodeObject:self.currentStatus forKey:#"currentStatus"];
[encoder encodeObject:self.targetName forKey:#"targetName"];
[encoder encodeObject:self.owner forKey:#"owner"];
CCLOG(#"SAVING LEVEL");
}
-(id)initWithCoder:(NSCoder *)decoder{
if (self = [super init]) {
self.objective = [[decoder decodeObjectForKey:#"objective"]retain];
self.isComplete = [[decoder decodeObjectForKey:#"isComplete"]retain];
self.goldReward = [[decoder decodeObjectForKey:#"goldReward"]retain];
self.xpReward = [[decoder decodeObjectForKey:#"xpReward"]retain];
self.missionID = [[decoder decodeObjectForKey:#"missionID"]retain];
self.targetName = [[decoder decodeObjectForKey:#"targetName"]retain];
self.owner = [[decoder decodeObjectForKey:#"owner"]retain];
CCLOG(#"LOADING LEVEL");
}
return self;
}
-(void)updateStatusForKill:(AI *)killedTarget{
CCLOG(#"WE KILLED: %# and OUR GOAL IS: %#",killedTarget.name,self.targetName);
if ([killedTarget.name isEqualToString:self.targetName]) {
[self setCurrentStatus:[NSNumber numberWithInt:[self.currentStatus intValue]+1]];
CCLOG(#"Current Status: %i Objective: %i", [self.currentStatus intValue],[self.objective intValue]);
if ([self.currentStatus intValue] == [self.objective intValue]) {
[self completeMission];
}
}
}
-(void)completeMission{
[self.owner setCoins:[NSNumber numberWithInt:[[self.owner coins]intValue] + [self.goldReward intValue]]];
[self.owner setXp:[NSNumber numberWithInt:[[self.owner xp]intValue] + [self.xpReward intValue]]];
CCLOG(#"complete");
[[self.owner missionList]removeObject:self];
}
#end
EDIT: The "owner" refers back to the object being saved. I think this is where the problem is, so I'm removing that and testing again.
EDIT: and that did nothing!
What you describe should "just work." In the encodeWithCoder method of your custom object, you would just encode the array object, and that should cause the array and it's contents to be encoded.
However, if any of the objects in the array do not support NSCoding, that will fail. My guess is that something in your array (or it's children or grandchildren) does not support NSCoding.
I ran into this problem when trying to save an array of Accounts that contain property values and another custom object. I couldn't save my data with your proposed solution because I was arbitrarily adding accounts to an array, and it wouldn't make sense to come up with unique identifiers for dynamically added accounts. I ended up nesting the NSCoding protocol:
In my AccountManager class:
- (void) saveAllAccounts {
[self deleteAllAccounts]; //Just removes whatever was previously stored there
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:accountArray] forKey:saveAccountsArrayKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
In my Account class:
- (void) encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:username forKey:#"username"];
[aCoder encodeObject:token forKey:#"token"];
[aCoder encodeObject:specialID forKey:#"special ID"];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:deviceArray];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"device array"];
}
- (id) initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self != nil) {
username = [[aDecoder decodeObjectForKey:#"username"] retain];
token = [[aDecoder decodeObjectForKey:#"token"] retain];
ecoID = [[aDecoder decodeObjectForKey:#"eco ID"] retain];
NSData *deviceArrayData = [[NSUserDefaults standardUserDefaults] objectForKey:#"device array"];
if (deviceArrayData != nil) {
NSArray *savedArray = [NSKeyedUnarchiver unarchiveObjectWithData: deviceArrayData];
if (savedArray != nil)
deviceArray = [[NSMutableArray alloc] initWithArray:savedArray];
}
}
return self;
}
In my AccountDevice class:
- (void) encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:pairingPassword forKey:#"pairing password"];
[aCoder encodeObject:devicePin forKey:#"device pin"];
}
- (id) initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self != nil) {
password = [[aDecoder decodeObjectForKey:#"password"] retain];
pin = [[aDecoder decodeObjectForKey:#"pin"] retain];
}
return self;
}
It might be a little buggy because I haven't finished testing it, but my preliminary tests were successful, and I think the concept is there.
It looks like you're using -encodeObject:forKey: and -decodeObjectForKey: even on values that aren't objects. For example, in your -initWithMissionID: you've got:
self.isComplete = NO;
which makes me think that complete is a BOOL property, but your -encodeObject:forKey: says:
[encoder encodeObject:self.isComplete forKey:#"isComplete"];
I think you probably want to call -encodeBool:forKey: instead, like this:
[encoder encodeBool:self.isComplete forKey:#"isComplete"];
On the other hand, I'd really expect some sort of warning if the problem were that simple. Do you get any warnings? It's harder to infer the types of your other properties, but look at each of your properties for the same kind of problem.
found a workaround using a Unique-Id system for each mission and saving the progress for each mission separately into NSUserDefaults which are just then loaded again. Not ideal, but it works. Thanks for everyone's help!

Objective-C: why my NSString is not retaining its value?

The problem lies within the 'initWithCoder' method. When I want to retrieve "Coins_Key" from where I saved it by calling the 'saveData' method in my 'main' class and I pass in the key "self.keyName," the value of keyName is 0.
//Class coins.h
#property (retain) NSString* keyName;
#property (retain) NSString* keyValue;
//Class coins.m
#synthesize keyName;
-(void) saveData:(NSString *)number: (NSString *)keyID
{
self.keyName = keyID;
self.keyValue = number;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
NSLog(#"Encoded keyName: %#", keyName);
[encoder encodeObject:keyValue forKey:keyName];
}
- (id)initWithCoder:(NSCoder *)decoder {
self.keyValue = [decoder decodeObjectForKey:self.keyName];
NSLog(#"Decoded Coins: %#", self.keyValue);
return self;
}
//Class main
[Coins *coin3 = [[Coins alloc] init];
[coin3 saveData:#"6" :#"Coins_Key"];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:coin3];
coin3 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
You're not quite grasping the encoder/decoder workflow.
Using the encodeObjectForKey: and decodeObjectForKey: methods properly, you should be passing as an argument the key that should be used to store the value. This key must remain constant.
You should also not require callers to provide the key your Coin object uses to store data. Take this simple example as a more correct/efficient method (assuming I understand the purpose of your class):
// Class Coins.h
#property (assign) int numberOfCoins;
// Class Coins.m
#define NUM_COINS_KEY #"NUM_COINS_KEY"
#synthesize numberOfCoins;
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) { // Use [super initWithCoder:decoder] here if your superclass supports it
self.numberOfCoins = [decoder decodeIntForKey:NUM_COINS_KEY];
NSLog(#"Decoded Coins: %d", self.numberOfCoins);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
NSLog(#"Encoded keyName: %#", keyName);
[encoder encodeInt:self.numberOfCoins forKey:NUM_COINS_KEY];
}
// Class main
Coins *coin = [[Coins alloc] init];
coin.numberOfCoins = 6;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:coin];
[coin release]; // If you're just playing around, this is probably overkill, but a good habit
coin = [NSKeyedUnarchiver unarchiveObjectWithData:data];

Need a simple sample on how to store/load my custom class for iOS development

I have the following objective-c class:
#interface LatLongData {
NSMutableString *_lat;
NSMutableString *_long;
NSMutableString *_altitude;
}
-(LatLongData*) initWithLat:(NSMutableString*) latitude Long:(NSMutableString*) longitude Altitude:(NSMutableString*) altitude;
#end
How can I store/load an instance of that object? I have tried working with code like this:
LatLongData *data = [[LatLongData alloc] initWithLat:_lat Long:_long Altitude:_altitude];
// Save
NSData *theData = [NSKeyedArchiver archivedDataWithRootObject:data];
[[NSUserDefaults standardUserDefaults] setObject:theData forKey:LatLongDataKey];
//...
// Load
NSData *loadedData = [[NSUserDefaults standardUserDefaults] dataForKey:LatLongDataKey];
LatLongData *ldata = (LatLongData *)[NSKeyedUnarchiver unarchiveObjectWithData:theData];
But I have no luck with that...
I know I should read everything about Core Data. I will but at this point it would be very nice to get my little sample to work.
You need to use NSCoding protocol with your class.
#interface LatLongData <NSCoding>
Here are a few protocol methods:
- (id)initWithCoder:(NSCoder *)aDecoder
{
if ((self = [super init])) {
lat = [[aDecoder decodeObjectForKey:#"lat"] copy];
long = [[aDecoder decodeObjectForKey:#"long"] copy];
alt = [[aDecoder decodeObjectForKey:#"alt"] copy];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:lat forKey:#"lat"];
[aCoder encodeObject:long forKey:#"long"];
[aCoder encodeObject:alt forKey:#"alt"];
}
P.S. You need to replace theData at your last line with loadedData

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.