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!
Related
I have a problem i cant get my head around. I have an instance of NSUserDefaults and have stored some values in there. One of these values is an array that contains the players scores (its a game) to be used in another display. After the game is finished, I update the array with the new score and reassign it to the NSUserDefaults, but for some reason it doesn't seem to be behaving correctly and in debug when i try to print the count into he console, its always 0.
Here is the code in question:
// // SAMHighScoreInputViewController.m // Tappity // // Created by Sam on 29/05/13. // Copyright (c) 2013 Sam. All rights reserved. //
#import "SAMHighScoreInputViewController.h"
#import "SAMScoreObject.h"
#interface SAMHighScoreInputViewController ()
#end
#implementation SAMHighScoreInputViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self; }
- (void)viewDidLoad {
self.defaults = [NSUserDefaults standardUserDefaults];
self.nameScoreLabel.text = [NSString stringWithFormat:#"%# - %#", [self.defaults objectForKey:#"lastName"], [self.defaults objectForKey:#"lastScore"]];
self.nameTextField.text = [self.defaults objectForKey:#"lastName"];
[super viewDidLoad]; // Do any additional setup after loading the view. }
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated. }
- (IBAction)textFieldValueChanged:(UITextField *)sender {
self.nameScoreLabel.text = [NSString stringWithFormat:#"%# - %#", self.nameTextField.text, [self.defaults objectForKey:#"lastScore"]];
}
- (IBAction)submitButtonTapped:(UIButton *)sender {
SAMScoreObject *score = [[SAMScoreObject alloc] init];
score.playerName = self.nameTextField.text;
score.score = [self.defaults integerForKey:#"lastScore"];
NSArray *scores = [self.defaults objectForKey:#"scoresArray"];
NSMutableArray *updatedArray = [scores mutableCopy];
[updatedArray addObject:score];
NSArray *updatedArrayScores = [updatedArray copy];
[self.defaults setObject:updatedArrayScores forKey:#"scoresArray"];
[self.defaults setObject:[self.defaults objectForKey:#"scoresArray"] forKey:#"scoresArray"];
[self.defaults synchronize];
NSLog(#"%#, %#", [self.defaults objectForKey:#"lastScore"], [self.defaults objectForKey:#"lastName"]);
NSLog(#"%i", [[self.defaults objectForKey:#"scoresArray"] count]);
[self dismissViewControllerAnimated:YES completion:nil];
} #end
You need to serialize your SAMScoreObject into an NSData object of some form before you can store it in the NSUserDefaults store.
From the documentation:
A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
The fact that you're storing an NSArray object isn't good enough, because the objects in that array aren't one of the supported types.
Where is the scoresArray ever created? You should probably lazily create it when you attempt to update it. For example:
NSArray *scores = [self.defaults objectForKey:#"scoresArray"];
if(scores == nil)
{
scores = [NSArray array];
}
NSMutableArray *updatedArray = [scores mutableCopy];
I'm kind of new to this, so it might be a beginners mistake. The problem is that when i send a message to a method it doesn't connect.
Here's the calling method:
-(BOOL) login:(LoginInfo *) info{
NSString *url = [NSString stringWithFormat:#"%#/%#?name=%#&password=%#", FAMORABLE_API_URL,FAMORABLE_API_ACTION_LOGIN, info.username, info.password];
NSDictionary* json = [self getJson:url];
NSString *token = [json objectForKey:#"Token"];
NSLog(#"results: %#", token);
LoginInfo *loginResult = [LoginInfo alloc];
loginResult.token = token;
//TODO
NSLog(#"test 1 %#", loginResult.token);
[clientService saveLoginInfo:loginResult];
return YES;
}
On the line above the last you can see I'm sending to saveLoginInfo in clientService which is declared in ClientService.h which is imported in this file.
-(void) saveLoginInfo:(LoginInfo *)info{
NSLog(#"test 2 %#", info);
NSLog(#"test 3%#", info.token);
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:info.token forKey:KEY_TOKEN];
NSString *string = [defaults stringForKey:KEY_TOKEN];
NSLog(#"test 4%#", string);
// save it
[defaults synchronize];
if (!(info.token)){
self.currentUser = nil;
self.isLoggedOn=false;
}else{
self.currentUser = info;
self.isLoggedOn=true;
}
}
This is the method being called. I've put out a bunch of logs, mostly as a safe if i did one wrong, but none of them are being executed...
Do you have any ideas of what might be wrong?
Thanks in advance
Tom
Is clientService in the line [clientService saveLoginInfo:loginResult]; an instance variable in your class?
If so, make sure that you have instantiated the object somewhere before you call it. (Either in your class's init method, or possibly viewDidLoad if it's a UIViewController.)
E.g.
- (void)viewDidLoad {
[super viewDidLoad];
clientService = [[MyClientService alloc] init];
}
- (id)init {
self = [super init];
if (self) {
clientService = [[MyClientService alloc] init];
}
return self;
}
(Make sure you also release the instance in the dealloc method.
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];
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
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. :)