NSArray mutability and arrayByAddingObject - objective-c

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

Related

Problems in getting an array from NSUserDefaults

I am making a game using cocos2d-iphone and would like to make a screen with best previous scores. I have two scenes. In the MainScene I made a global variable for storing a score, that is obviously changing during the game; one mutable array and one simple array that will be a duplicate for the mutable one:
NSInteger _scoreValue;
NSMutableArray *_scoresMutable;
NSArray *_scores;
In the same class, when game ends, I add new score to the mutable array, make a static duplicate and save it in NSUserDefaults:
[_scoresMutable addObject:#(_scoreValue)];
_scores=[NSArray arrayWithArray:_scoresMutable];
[[NSUserDefaults standardUserDefaults] setObject:_scores forKey:#"gameScores"];
Then, in other class of scene with scores called bestscores(I don't know how is better, but it was easier for me to make just a new scene, because I am using SpriteBuilder) I import MainScene.h just in case and make a label.
At the moment I am trying to get all scores from NSUserDefaults, to sort it and to show second biggest value. But it always shows 0 (label is empty by default). So, how to make that's all right?
- (void)didLoadFromCCB {
NSSet *numberSet = [NSSet setWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:#"gameScores"]];
NSArray *sortedNumbers = [[numberSet allObjects] sortedArrayUsingDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"self" ascending:NO] ]];
NSNumber *secondHighest;
if ([sortedNumbers count] > 1){
secondHighest = sortedNumbers[1];
}
[_secondBiggestLabel setString:[NSString stringWithFormat:#"%ld",(long)secondHighest]];
}
`
EDIT : synchronize didn't help. Maybe I need to write something else to get access to NSUserDefaults from another one class?
When you set the object you have to follow it with a call to synchronize. That will actually save it.
[[NSUserDefaults standardUserDefaults] synchronize];
Are you sure you've initialized that '_scoresMutable' array?
Also, if the code from the below section is ran on a separate launch than when the first section of code is ran, it's possible you're terminating the app before the defaults can write to disk. Call 'synchronize' on the NSUserDefaults instance to fix that.

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.

Retrieving NSArray From an NSDictionary

I am trying to get an array full of my data, I keep getting an BAD_ACCESS error when I run this though at the calling the array which I have not included here but I even commented that code out and tried just calling it to the log and still get the BAD_ACCESS error. The array is stored in a dictionary that contains a one key that is a number. I am not sure what I am doing wrong here.
ISData *is = [[ISData alloc] init];
NSDictionary *dic = [is getData:#"right" : isNumber];
NSArray *array = [[NSArray alloc] initWithArray:[dic valueForKey:#"2"]];
NSString *out = [array objectAtIndex:0];
How the dictionary is created:
NSNumber* key = [NSNumber numberWithInt:isNumber];
NSArray *values = [NSArray arrayWithObjects:[NSString stringWithUTF8String:name], [NSString stringWithUTF8String:desc], [NSString stringWithUTF8String:intent], nil];
[dic setObject:values forKey:key];
You don't say exactly where it crashes, and you don't have an obvious crashing bug here, so it's hard to diagnose your actual issue. This could be a memory management thing that's outside the code you've presented. But a couple things are going on here that are suspicious:
You should never have a bare [MyClass alloc] without the -init call. Your init should call super's init, which is responsible for setting up the new object.
Your -valueForKey: should be -objectForKey:. The difference is probably unimportant in this case, but the former is used for "KVC" coding, which you're not using. If you set it as object, get it as object.
Your #"2" as the key into the dictionary doesn't match your input, which is an NSNumber. NSNumbers are not string versions of numbers, so you're unlikely to find any value there. Instead, use the same [NSNumber numberWithInt:2] pattern.
It is most likely that your array is empty. You can try print NSLog(#"count = %d", array.count); to see if that's the case.
If your dic is set up in the second block of code, then what's that NSDictionary *dic = [is getData:...] thing in the first block?
And is there a reason you cannot set up your array directly? Is there a reason for you to use a dictionary when it has only one 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.

How to append two NSMutableArray's in Iphone sdk or append an NSArray With NSMutableArray?

I need to append two NSMUtableArray's can any one suggest me how it possible?
My code is:
NSMutableArray *array1 = [appDelegate getTextList:1];
NSArray *array2 = [appDelegate getTextList:2];
[array1 addObjectsFromArray:array2];//I am getting exception here.
Anyone's help will be much appreciated.
Thanks all,
Lakshmi.
What's probably happening, is that your [appDelegate getTestList:1] is not actually returning a NSMutableArray, but a NSArray. Just typecasting the array as mutable by holding a pointer to it like that will not work in that case, instead use:
NSMutableArray *array1 = [[appDelegate getTextList:1] mutableCopy];
NSArray *array2 = [appDelegate getTextList:2];
[array1 addObjectsFromArray:array2];
Or you could store the 'textList' variable that you have in your appDelegate as an NSMutableArray in the first place. I am assuming that you have an NSArray of NSArrays (or their mutable versions). Eg.
// In the class interface
NSMutableArray *textLists;
// In the function in which you add lists to the array
NSMutableArray *newTextList;
[self populateArray:newTextList]; // Or something like that
[textLists addObject:newTextList];
Note: that you will probably have a different workflow, but I hope that you get the idea of storing the actual lists as NSMutableArrays.
Another Note: the second method WILL modify in place the NSMutableArray that [appDelegate getTextList:1]; returns
Try this:
NSMutableArray *result =
[[appDelegate getTextList:1] mutableCopy]
addObjectsFromArray:[appDelegate getTextList:2]];
You're getting the exception because you're trying to send mutating messages to an immutable array.