NSUserDefaults on iPhone SDK not returning nil - cocoa-touch

I'm using NSUserDefaults for a Settings Application. This is a pristine copy and I have not at all added anything to the filesystem for this particular setting. I expect it to be nil, but on the contrary..
If I do something like the following:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ( [defaults valueForKey:kIdentifierKey] == nil ) {
return YES;
}
else {
NSLog(#"we have a value. return NO. Value is: %#", [defaults objectForKey:kIdentifierKey]);
return NO;
}
Yet in NSLog, nothing shows where you'd expect the value. The else block does indeed get executed. Is there any reason why this is NOT nil?

valueForKey: is kind of a complex method in all the places it searches. I find it more reliable to use the getters for specific datatypes when dealing with defaults.
if ([defaults boolForKey:kIdentifierKey]) [self doSomething];
Or you could use objectForKey: which should return nil as you expect. In fact, you do this when you create your NSLog statement. And the string description of nil is #"" so your output is as you would expect.
Also, you are testing and logging different keys here:
[defaults valueForKey:kIdentifierKey];
[defaults objectForKey:kGroupIdentifierKey];
Making sure you are testing and logging what you think you are.
Test from a new iPhone project:
if (![[NSUserDefaults standardUserDefaults] valueForKey:#"foobar"]) NSLog(#"is not yet defined");
if (![[NSUserDefaults standardUserDefaults] objectForKey:#"foobar"]) NSLog(#"is not yet defined");
if ([[NSUserDefaults standardUserDefaults] valueForKey:#"foobar"] == nil) NSLog(#"valueForKey is nil");
if ([[NSUserDefaults standardUserDefaults] objectForKey:#"foobar"] == nil) NSLog(#"objectForKey is nil");
It outputs:
is not yet defined
is not yet defined
valueForKey is nil
objectForKey is nil
I think you have something else going on here.

Based on the docs....
"A default’s value 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.
For more details, see User Defaults
Programming Topics for Cocoa."
Also, [defaults valueForKey:] is not an instance method according to the documentation, so the result is most likely undefined. If you use [defaults objectForKey:] it will return nil if the object associated with kIdentifierKey is not found.

Related

If I use "registerDefaults" to register defaults, I can not remove the defaults which I registered

NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:#"content", #"myKey", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"myKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(#"%#",[[NSUserDefaults standardUserDefaults] objectForKey:#"myKey"]);
Log :
2012-12-17 17:09:05.062 browserHD[4075:c07] content
Why the result value "content"? I want the result value "nil".
Yes you are correct...
This may not be the perfect solution,. but a little bit of tweak to solve the problem.
If you use this way it works !!!
- (IBAction)set:(id)sender {
NSMutableDictionary *aDict=[NSMutableDictionary new];
[aDict setObject:_myText.stringValue forKey:#"key"];
[[NSUserDefaults standardUserDefaults] registerDefaults:aDict];
}
- (IBAction)remove:(id)sender {
NSMutableDictionary *aDict=[NSMutableDictionary new];
[aDict setObject:#"null" forKey:#"key"];
[[NSUserDefaults standardUserDefaults] registerDefaults:aDict];
}
- (IBAction)get:(id)sender {
NSLog(#"-> %#",[[NSUserDefaults standardUserDefaults]objectForKey:#"key"]);
}
The registerDefaults affects a different "level" of user defaults than the setObject:forKey: or the removeObjectForKey. It goes into the global user defaults which is a level higher than the standard user defaults. "Global" being a misnomer because it is still local to your app.
You cannot remove values from the global user defaults with this method.
Global Defaults (NSGlobalDomain)
-> User Defaults (NSRegistrationDomain)
If a value is in user values then it overrides the global value. If not then the value from Global Defaults is taken. Adding and removing values only affects the User Defaults level.
Apparently you cannot get rid of the global defaults because if you remove the user default the global one will always "shine through". But you could set the object value for something you want to "replace with nil" to [NSNull null]. Then in your code you'd check for this case: either the default is nil or it is [NSNull null] which would tell you that the value is not set.

Use of #synthesize and value class type checking

This is probably a long shot, but I've got objects with a lot of properties. The values of these objects are populated from NSDictionary's created from a database request. Because of this, there may be NSNull values contained in those NSDictionaries that will automatically get assigned to the properties. I need the properties to automatically discard values/objects that aren't of the correct type. Currently I do it like this:
- (void) setViewID:(NSString *)viewID{
if (!viewID || [viewID isKindOfClass:[NSString class]]) _viewID = viewID;
}
But that ends up being a lot of extra code when I've got 30-50 properties. Is there a way to synthesize this behavior? It seems like it would be a common enough requirement, but I can't seem to find a way to do it aside from writing it all out.
Why not check for NSNull when you are going through the dictionary? E.g.
for (NSString *key in dictionary) {
id value = [dictionary objectForKey:key];
if (value == [NSNull null]) {
value = nil;
}
[self setValue:value forKey:key];
}

Strange "mutating method sent to immutable object" error when adding an object to a mutable array

This is a strange one...
Periodically I am checking if the user has achieved a new top score in my app. Here is my code:
- (void) newTopScore
{
// pull old top score from the database
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *getBoardsAndScores = [defaults dictionaryRepresentation];
NSMutableArray *boardsAndScores = [[getBoardsAndScores objectForKey:#"boardsAndScores"] mutableCopy];
NSMutableDictionary *boardAndScoreObject = [[boardsAndScores objectAtIndex:gameBoardIndex] mutableCopy];
NSString *topscore = [boardAndScoreObject objectForKey:#"topscore"];
NSLog(#"topscore in value: %i", [topscore intValue]);
if ([topscore intValue] == 0)
{
NSString *topscoreString = [[NSString alloc] initWithFormat:#"%i", [self countCurrentPegs]];
NSMutableArray *mutableBoardsAndScores = [boardsAndScores mutableCopy];
[[mutableBoardsAndScores objectAtIndex:gameBoardIndex] setObject:topscoreString forKey:#"topscore"];
[defaults setObject:mutableBoardsAndScores forKey:#"boardsAndScores"];
[defaults synchronize];
}
else if ([self countCurrentPegs] < [topscore intValue])
{
NSString *topscoreString = [[NSString alloc] initWithFormat:#"%i", [self countCurrentPegs]];
NSMutableArray *mutableBoardsAndScores = [boardsAndScores mutableCopy];
[[mutableBoardsAndScores objectAtIndex:gameBoardIndex] setObject:topscoreString forKey:#"topscore"];
[defaults setObject:mutableBoardsAndScores forKey:#"boardsAndScores"];
[defaults synchronize];
}
}
Sometimes the code works just fine, both for the if and the else. Sometimes though I get the following error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[__NSCFDictionary setObject:forKey:]:
mutating method sent to immutable object'
I've tried setting break points and stepping through the program line by line. The crash seems somewhat random and I can't work out a pattern. It usually crashes on the second...
[[mutableBoardsAndScores objectAtIndex:gameBoardIndex] setObject:topscoreString forKey:#"topscore"];
...line.
The object IS mutable. Or at least it was. Why is it changing?
Any help of advice would be very appreciated.
Thanks.
[[[mutableBoardsAndScores objectAtIndex:gameBoardIndex] mutableCopy] setObject:topscoreString forKey:#"topscore"];
Could you test this? (added the mutableCopy for the gameBoard);
EDIT
could you change the above to:
NSMutableArray *gameBoard = [[mutableBoardsAndScores objectAtIndex:gameBoardIndex] mutableCopy];
[gameBoard setObject:topscoreString forKey:#"topscore"];
[mutableBoardsAndScores removeObjectAtIndex:gameBoardIndex];
[mutableBoardsAndScores insertObject:gameBoard atIndex:gameBoardIndex];
Could you try this? It might not be the most beautiful solution but i believe it will work.
NSUserDefaults does not store the objects you set to it. It stores the data. Effectively, it's making a copy. And any collection object you retrieve from NSUserDefaults will be immutable.
Edited to say: also, when you make a mutable copy of a collection, it's a shallow copy. You get a mutable collection holding the same objects as the original collection. If the original collection held immutable objects, then so will the new collection.
If you need to do a deep copy of a property-list compatible hierarchy of objects, getting mutable objects back, you can use NSPropertyListSerialization. Combine +dataWithPropertyList:format:options:error: with +propertyListWithData:options:format:error: and the NSPropertyListMutableContainers or NSPropertyListMutableContainersAndLeaves options.

NSArray mutability and arrayByAddingObject

I thought I had a good understanding of Objects and mutability in Objective-C but I've noticed something curious with NSArray.
If I have the following that doesn't work:
NSArray *myArray = [ [[NSUserDefaults standardUserDefaults] arrayForKey:someKey] arrayByAddingObject:myObject ];
"myObject" is never added to "myArray".
However, the following works:
NSArray *someArray = [[NSUserDefaults standardUserDefaults] arrayForKey:someKey];
NSArray *myArray = [someArray arrayByAddingObject:myObject];
What's the explanation for this? I know that NSArray is not mutable but since it is during the initial assignment, it should work since either way seems equivalent.
[[NSUserDefaults standardUserDefaults] arrayForKey:someKey] returns an NSArray
someArray is an NSArray
And so, an NSArray is derived simply from [NSArray arrayByAddingObject:Object]
Thanks!
EDIT 1
I am simply wanting to store the NSArray back to NSUserDefaults:
[[NSUserDefaults standardUserDefaults] setObject:myArray forKey:someKey];
It was never storing it properly, and so when I debugged I saw that "myObject" was never being added when the array was created.
Note: I have only been testing with [[NSUserDefaults standardUserDefaults] arrayForKey:someKey] returning a empty/nil array
EDIT 2
I've isolated the issue a bit; this only happens if [NSUserDefaults standardUserDefaults] arrayForKey:someKey] returns "nil" (basically, the array doesn't exist in NSUserDefaults).
I'm still curious to know though why it's not an issue for the 2nd code solution I have.
How are you checking to see if myObject is ever added? Both code examples look fine, and I've certainly used the former with no issues. But all of your assumptions are correct. The issue, if indeed there is one, lies elsewhere.
That should be the expected behaviour
If you call a method on nil in Objective-C, you always get nil (or NO/0) as your return value
In this case, when calling NSArray *myArray = [ [[NSUserDefaults standardUserDefaults] arrayForKey:someKey] arrayByAddingObject:myObject ];, if someKey isn't set in NSUserDefaults, then you're calling [nil arrayByAddingObject:] and so would expect to get nil back

NSKeyedUnarchiver does not call initWithCoder

I've used the following example:
game state singleton cocos2d, initWithEncoder always returns null
Instead of using a file, I use an NSData object i am getting from NSUserDefaults. It is not null, but rather have data in it that I believe is the correctly encoded data and for some reason when I run unarchiveObjectWithData it does not call initWithCoder as I put an NSLog on the first line.
Ideas?
+ (void)loadState {
#synchronized([AppStateManager class]) {
if (!sharedAppStateManager) {
[AppStateManager sharedManager];
}
NSUserDefaults *udefs = [NSUserDefaults standardUserDefaults];
NSData *archivedData = [udefs objectForKey:#"AppState"];
NSLog(#"going to unarchive");
[NSKeyedUnarchiver unarchiveObjectWithData:archivedData];
NSLog(#"unarchive complete");
}
}
- (id)initWithCoder:(NSCoder *)decoder {
NSLog(#"init coder");
self = [super init];
if (self != nil) {
[self setUsername:[decoder decodeObjectWithKey:#"username"]];
}
return self;
}
Solved.
The example posted at the link above initializes the object via "sharedInstance" using the allocWithZone method. If the object exists it returns nil.
the NSKeyedUnarchiver tries to alloc AGAIN via allocWithZone and since the object has already been initialized earlier in that very same method, or sometimes before hand the unarchiver gets a nil instead of an allocated object.
Hope this helps someone.
I don't know why, but after I do this to obtain my NSData of the archive (the contents of which i think is inconsequential):
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSData *encodedConfig = [userDefaults valueForKey:kConfiguration];
When I execute this, it returns nil.
NSArray *configs = [NSKeyedUnarchiver unarchiveObjectWithData:encodedConfig];
But when I execute this, I get the expected, previously archived, array of objects.
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedConfig];
NSArray *configs = [unarchiver decodeObject];
I expect identical behavior from these two approaches. My only response is avoid unarchiveObjectWithData:.
This isn't so much an answer but a bit of additional info I found while trying to work a similar issue out. I guess I am still under rookie status, so this is the only way I could find to contribute to this post.
Anyways...
Thanks so much for banging through this issue...especially over here:
http://www.cocos2d-iphone.org/forum/topic/11327
I was having a problem with a mutable array working using the method you had worked out where it would switch to an immutable array after I unarchived it from the file.
Casting the returned array as a mutable version did not work either.
I end up adding objectsFromArray to my mutable array and this solved my problems.
Worked:
[self.mutableArray addObjectsFromArray:[decoder decodeObjectForKey:#"MutableArray"]];
Neither of these worked:
self.mutableArray = [decoder decodeObjectForKey:#"MutableArray"];
self.mutableArray = (NSMutableArray *)[decoder decodeObjectForKey:#"MutableArray"];