A mutation method sent to an immutable object closes the application
In my code, when the user completes a level, it will unlock the next level (of 15 levels).
However, I noticed that when I complete the level, the first time, it works, however, when I go back to it and try again, it crashes with this error;[__NSCFArray replaceObjectAtIndex:withObject:]: mutating method sent to immutable object
This piece of code:
//Unlock Next Level
if (levelNumber != 15) {
[m_appDelegate.levels replaceObjectAtIndex:levelNumber withObject:[NSNumber numberWithBool:NO]];
[m_appDelegate saveLevels];
}
If I remove this line,
[m_appDelegate.levels replaceObjectAtIndex:levelNumber withObject:[NSNumber numberWithBool:NO]];
The app then doesn't crash, but of course it doesn't unlock further levels.
Some links below that might help;
int levelNumber;
@property (nonatomic, readwrite) int levelNumber;
- (void) actionLockedLevel:(id)sender {
selectedLevel = ((CCNode*)sender).tag;
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Level Locked" message:@"Level is locked. Pass the previous level first." delegate:nil cancelButtonTitle:@"No" otherButtonTitles:nil];
[alert show];
}
- (void) actionUnlockedLevel:(id)sender {
CCScene *scene = [CCScene node];
TimedLevel *layer = [TimedLevel node];
layer.levelNumber = ((CCNode*)sender).tag;
[scene addChild:layer];
[[CCDirector sharedDirector] replaceScene:scene];
}
The levels are called in AppDelegate.m:
- (void) loadLevels {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:@"LEVELS"] == nil) {
levels = [[NSMutableArray alloc] initWithObjects:
[NSNumber numberWithBool:NO],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],
[NSNumber numberWithBool:YES],nil];
} else {
self.levels = [defaults objectForKey:@"LEVELS"];
}
if ([defaults objectForKey:@"LEVELS_PIZZAS"] == nil) {
levelsPizzas = [[NSMutableArray alloc] initWithObjects:
[NSNumber numberWithInteger:200],
[NSNumber numberWithInteger:250],
[NSNumber numberWithInteger:300],
[NSNumber numberWithInteger:350],
[NSNumber numberWithInteger:400],
[NSNumber numberWithInteger:450],
[NSNumber numberWithInteger:500],
[NSNumber numberWithInteger:550],
[NSNumber numberWithInteger:600],
[NSNumber numberWithInteger:650],
[NSNumber numberWithInteger:700],
[NSNumber numberWithInteger:800],
[NSNumber numberWithInteger:900],
[NSNumber numberWithInteger:1000],
[NSNumber numberWithInteger:1100],nil];
} else {
self.levelsPizzas = [defaults objectForKey:@"LEVELS_PIZZAS"];
}
if ([defaults objectForKey:@"COLLECTED_PIZZAS"] == nil) {
self.nCurClickAmounts = 0;
} else {
self.nCurClickAmounts = [[defaults objectForKey:@"COLLECTED_PIZZAS"] intValue];
}
[defaults synchronize];
}
- (void) saveLevels {
[[NSUserDefaults standardUserDefaults] setObject:levels forKey:@"LEVELS"];
[[NSUserDefaults standardUserDefaults] setObject:levelsPizzas forKey:@"LEVELS_PIZZAS"];
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInt:nCurClickAmounts] forKey:@"COLLECTED_PIZZAS"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
source to share
Here's the problematic line:
self.levels = [defaults objectForKey:@"LEVELS"];
This gives an immutable NSArray
, even if you kept the modified default array. You need to call mutableCopy
if you want your array to remain mutable:
self.levels = [[defaults objectForKey:@"LEVELS"] mutableCopy];
The same goes for an array levelsPizzas
.
source to share
The problem is what you think of NSUserDefaults
as different NSDictionary
.
After rebooting the app or syncing the repository, NSUserDefaults may have changed all of your mutable objects (for example NSMutableArray
) to NSArray
.
Any classes stored in NSUserDefaults
that are not NSNumber,NSString,NSDate,NSArray
or are also persisted NSDictionary
.
So don't forget to save them to NSUserDefaults or handle the case when this change happens.
In your code, you just need to change:
self.levels = [defaults objectForKey:@"LEVELS"];
to
self.levels = [[defaults objectForKey:@"LEVELS"] mutableCopy];
source to share