Reputation: 26526
I have read that an array retrieved from NSUserDefaults is immutable. If I have an array of dictionaries, and I want to update the object for a key on one of those dictionaries, do I have to make a mutable copy of the whole array and/or the dictionary?
Given an array stored for key 'Teams', containing multiple dictionaries each with a key 'Innings', I am using:
NSMutableArray *teams = [[[NSUserDefaults standardUserDefaults] objectForKey:@"Teams"] mutableCopy];
NSMutableDictionary *teamDictionary = [teams objectAtIndex:_selectedIndex.row];
[teamDictionary setObject:@99 forKey:@"Innings"];
[[NSUserDefaults standardUserDefaults] setObject:teams forKey:@"Teams"];
[[NSUserDefaults standardUserDefaults] synchronize];
but am receiving:
mutating method sent to immutable object
What is the correct approach here?
Upvotes: 0
Views: 1439
Reputation: 802
From Apple Docs: Values returned from NSUserDefaults are immutable, even if you set a mutable object as the value. For example, if you set a mutable string as the value for "MyStringDefault", the string you later retrieve usingstringForKey: will be immutable.
The Apple docs have always said this. In practice, dictionaries and arrays have always been mutable, despite Apple's warning, as long as you used synchronize. The difference in Mountain Lion is that now, if you read/write a deeply nested dictionary, those deeply nested objects are not saved to NSUserDefaults.
They may even look like they've been saved because you can read the values back right before you exit your app. Subtly, they are not there when you relaunch.
Even worse, making a mutableCopy doesn't solve the problem. Only making a mutableCopyDeepPropertyList solves the problem (see solution below)
Before Mountain Lion, code like this worked, even though the docs suggest that it shouldn't
NSMutableDictionary *parentDict = [[NSUserDefaults standardUserDefaults] objectForKey:@"parentDict"];
NSLog( @"starting up... %@", parentDict );
if ( !parentDict )
{
NSMutableDictionary *childDict = [NSMutableDictionary dictionaryWithObject: @"1" forKey: @"MyNumber1"];
parentDict = [NSMutableDictionary dictionaryWithObject:childDict forKey: @"childDict"];
[[NSUserDefaults standardUserDefaults] setObject: parentDict forKey: @"parentDict"];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog( @"first time run... %@", parentDict );
exit(0);
}
NSMutableDictionary *childDict = [parentDict objectForKey: @"childDict"];
[childDict removeObjectForKey:@"MyNumber2"];
[childDict setObject: @"2" forKey: @"MyNumber2"];
[[NSUserDefaults standardUserDefaults] setObject: parentDict forKey: @"parentDict"];
[[NSUserDefaults standardUserDefaults] synchronize];
// Now read the value back to verify it:
parentDict = [[NSUserDefaults standardUserDefaults] objectForKey:@"parentDict"];
NSLog( @"exiting... %@", parentDict );
exit(0);
1st run:
2013-07-26 18:01:55.064 Mbox Director-[Debug][15391:303] starting up... (null) 2013-07-26 18:01:55.210 Mbox Director-[Debug][15391:303] first time run... { childDict = { MyNumber1 = 1; }; }
2nd run (everything looks correct):
2013-07-26 18:02:54.999 Mbox Director-[Debug][15510:303] starting up... { childDict = { MyNumber1 = 1; }; } 2013-07-26 18:02:55.000 Mbox Director-[Debug][15510:303] exiting... { childDict = { MyNumber1 = 1; MyNumber2 = 2; }; }
Results in Mountain Lion 3rd run (notice, MyNumber2 missing when starting up...):
2013-07-26 17:39:48.760 Mbox Director-[Debug][15047:303] starting up... { childDict = { MyNumber1 = 1; }; } 2013-07-26 17:39:48.760 Mbox Director-[Debug][15047:303] exiting... { childDict = { MyNumber1 = 1; MyNumber2 = 2; }; }
Results in Lion: 3rd run (notice, MyNumber2 got saved...): 2013-07-26 17:36:23.886 Mbox Director-[Debug][17013:120b] starting up... {
childDict = { MyNumber1 = 1; MyNumber2 = 2; }; } 2013-07-26 17:36:23.938 Mbox Director-[Debug][17013:120b] exiting... { childDict = { MyNumber1 = 1; MyNumber2 = 2; }; }
// This function makes a deep mutable copy. NSDictionary and NSArray mutableCopy does not create a DEEP mutableCopy.
// We accomplish a deep copy by first serializing the dictionary
// to a property list, and then unserializing it to a guaranteed deep copy.
// It requires that your array is serializable, of course.
// This method seems to be more bulletproof than some of the other implementations
// available on the web.
//
// Follows copy rule... you are responsible for releasing the returned object.
// Returns nil if not serializable!
id mutableCopyFromPlist( id plist )
{
NSError *error = nil;
@try
{
#ifdef MAC_OS_X_VERSION_10_6
NSData *binData = [NSPropertyListSerialization dataWithPropertyList:plist
format:NSPropertyListBinaryFormat_v1_0
options:0
error:&error];
NSString *errorString = [error localizedDescription];
#else
NSString *errorString = nil;
NSData *binData = [NSPropertyListSerialization dataFromPropertyList:plist
format:NSPropertyListBinaryFormat_v1_0
errorDescription:&errorString];
#endif
if (errorString || !binData )
{
DLogErr( @"error serializing property list %@", errorString );
}
else
{
#ifdef MAC_OS_X_VERSION_10_6
NSError *error = nil;
id deepCopy = [NSPropertyListSerialization
propertyListWithData:binData
options:NSPropertyListMutableContainersAndLeaves
format:NULL
error:&error];
errorString = [error localizedDescription];
#else
id deepCopy = [NSPropertyListSerialization
propertyListFromData:binData
mutabilityOption:NSPropertyListMutableContainersAndLeaves
format:NULL
errorDescription:&errorString];
#endif
[deepCopy retain]; // retain this so that we conform to the 'copy rule'... our function name contains the work 'Copy'
if (errorString)
{
DLogErr( @"error serializing property list %@", errorString );
}
else
{
return deepCopy;
}
}
}
@catch (NSException *exception )
{
DLogErr( @"error serializing property list %@", [error localizedDescription] );
}
return nil; // couldn't make a deep copy... probably not serializable
}
@implementation NSDictionary (VNSDictionaryCategory)
// This function makes a deep mutable copy. NSDictionary's mutableCopy does not create a DEEP mutableCopy.
// We accomplish a deep copy by first serializing the dictionary
// to a property list, and then unserializing it to a guaranteed deep copy.
// It requires that your dictionary is serializable, of course.
// This method seems to be more bulletproof than some of the other implementations
// available on the web.
//
// Follows copy rule... you are responsible for releasing the returned object.
// Returns nil if not serializable!
-(NSMutableDictionary *)mutableCopyDeepPropertyList
{
return mutableCopyFromPlist( self );
}
@end
#pragma mark -
@implementation NSArray (VNSArrayCategory)
// This function makes a deep mutable copy. NSDictionary's mutableCopy does not create a DEEP mutableCopy.
// We accomplish a deep copy by first serializing the dictionary
// to a property list, and then unserializing it to a guaranteed deep copy.
// It requires that your array is serializable, of course.
// This method seems to be more bulletproof than some of the other implementations
// available on the web.
//
// Follows copy rule... you are responsible for releasing the returned object.
// Returns nil if not serializable!
-(NSMutableArray *)mutableCopyDeepPropertyList
{
return mutableCopyFromPlist( self );
}
@end
Usage:
NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] objectForKey:@"mydictionary"];
dict = [[dict mutableCopyDeepPropertyList] autorelease];
Upvotes: 0
Reputation: 26526
The solution was to use a mutable copy of the NSDictionary also. The mutable copy of the array is not a 'deep copy' - the dictionaries inside remain immutable.
So I had to also make a mutable copt of the dictionary, update it, and then replace the original dictionary with the copy.
NSMutableArray *teams = [[[NSUserDefaults standardUserDefaults] objectForKey:@"Teams"] mutableCopy];
NSMutableDictionary *teamDictionary = [[teams objectAtIndex:_selectedIndex.row] mutableCopy];
[teamDictionary setObject:@99 forKey:@"Innings"];
[teams replaceObjectAtIndex:_selectedIndex.row withObject:teamDictionary];
[[NSUserDefaults standardUserDefaults] setObject:teams forKey:@"Teams"];
[[NSUserDefaults standardUserDefaults] synchronize];
Upvotes: 2