Ben Packard
Ben Packard

Reputation: 26526

Updating a key in NSUserDefaults dictionary (inside array)

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

Answers (2)

Keith Knauber
Keith Knauber

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

Ben Packard
Ben Packard

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

Related Questions