Thomson Comer
Thomson Comer

Reputation: 3919

Why is NSUserDefaults unwilling to save my NSDictionary?

I'm using KVC to serialize an NSObject and attempt to save it to NSUserDefaults, which is giving me an Attempt to insert non-property value when I try to store my NSDictionary.

Following are the properties of the object in question, MyClass:

@interface MyClass : NSObject
@property (copy,nonatomic) NSNumber* value1;
@property (copy,nonatomic) NSNumber* value2;
@property (copy,nonatomic) NSString* value3;
@property (copy,nonatomic) NSString* value4;
@property (copy,nonatomic) NSString* value5;
@property (copy,nonatomic) NSString* value6;
@property (copy,nonatomic) NSString* value7;
@property (copy,nonatomic) NSString* value8;
@end

When it is time to save MyClass it occurs here:

-(void)saveMyClass
{
  NSArray* keys = [NSArray arrayWithObjects:
                @"value1",
                @"value2",
                @"value3",
                @"value4",
                @"value5",
                @"value6",
                @"value7",
                @"value8",
                nil];
  NSDictionary* dict = [self dictionaryWithValuesForKeys:keys];
  for( id key in [dict allKeys] )
  {
    NSLog(@"%@ %@",[key class],[[dict objectForKey:key] class]);
  }
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  [defaults setObject:dict forKey:[NSString stringWithString:kMyClassKey]];
  [defaults synchronize];
}

which produces this output:

2012-02-23 19:35:27.518 MyApp[10230:40b] __NSCFConstantString __NSCFNumber
2012-02-23 19:35:27.519 MyApp[10230:40b] __NSCFConstantString __NSCFNumber
2012-02-23 19:35:27.519 MyApp[10230:40b] __NSCFConstantString __NSCFString
2012-02-23 19:35:27.519 MyApp[10230:40b] __NSCFConstantString __NSCFString
2012-02-23 19:35:27.520 MyApp[10230:40b] __NSCFConstantString __NSCFString
2012-02-23 19:35:27.520 MyApp[10230:40b] __NSCFConstantString __NSCFString
2012-02-23 19:35:27.520 MyApp[10230:40b] __NSCFConstantString __NSCFString
2012-02-23 19:35:27.520 MyApp[10230:40b] __NSCFConstantString NSNull
2012-02-23 18:38:48.489 MyApp[9709:40b] *** -[NSUserDefaults setObject:forKey:]: Attempt to insert non-property value '{
    value1 = "http://www.google.com";
    value2 = "MyClassData";
    value3 = 8;
    value4 = "<null>";
    value5 = "http://www.google.com";
    value6 = 1;
    value7 = "http://www.google.com";
    value8 = 4SY8KcTSGeKuKs7s;
}' of class '__NSCFDictionary'.  Note that dictionaries and arrays in property lists must also contain only property values.`

As you can see, all of the objects in the dict are property list values and all of its keys are NSString*. What trivia am I lacking in order to execute this? Or should I give up and use writeToFile or similar?

Upvotes: 14

Views: 10945

Answers (3)

Berk
Berk

Reputation: 1329

If you need to store;

  • data from a custom object,
  • or an array of custom objects

you can use NSKeyedArchiver methods. You can check leviathan's answer for this method.


However, if you are trying to store;

  • a dictionary that contains either NSString or NSNumber (like a dictionary converted from a JSON service response),
  • or array of this kind of dictionary

you don't need to use NSKeyedArchiver. You can use user defaults.


In my case, when I retrieve the dictionary from user defaults it was returning nil, so I thought NSUserDefaults is unwilling to save my dictionary.

However, it was saved, but I was using the wrong getter method to retrieve it from user defaults;

[[NSUserDefaults standardUserDefaults] stringForKey:<my_key>]

Please make sure you used either;

[[NSUserDefaults standardUserDefaults] dictionaryForKey:<my_key>]

or;

[[NSUserDefaults standardUserDefaults] objectForKey:<my_key>]

before checking any other possible reason.

Upvotes: -2

leviathan
leviathan

Reputation: 11161

I usually archive and unarchive dictionaries when saving them to the user defaults. This way you don't have to manually check for NSNull values.

Just add the following two methods to your code. Potentially in a category.

- (BOOL)archive:(NSDictionary *)dict withKey:(NSString *)key {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSData *data = nil;
    if (dict) {
        data = [NSKeyedArchiver archivedDataWithRootObject:dict];
    }
    [defaults setObject:data forKey:key];
    return [defaults synchronize];
}

- (NSDictionary *)unarchiveForKey:(NSString *)key {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSData *data = [defaults objectForKey:key];
    NSDictionary *userDict = nil;
    if (data) {
        userDict = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    }
    return userDict;
}

Then you can archive any dictionary like this (assuming the method are available in the class):

NSDictionary *dict = ...;
[self archive:dict withKey:@"a key of your choice"];

and retrieve it later on again like this:

NSDictionary *dict = [self unarchiveForKey:@"a key of your choice"];

Upvotes: 21

Thomson Comer
Thomson Comer

Reputation: 3919

Props to Kevin who suggested printing the values, of course one of which is of type NSNull which is not a property list value. Thanks!

The kludgy solution to my problem - iterate over the keys of the dictionary I just produced so conveniently with dictionaryWithValuesForKeys and remove those of type NSNull. sigh

NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithDictionary:[self dictionaryWithValuesForKeys:keys]];
for( id key in [dict allKeys] )
{
    if( [[dict valueForKey:key] isKindOfClass:[NSNull class]] )
    {
        // doesn't work - values that are entered will never be removed from NSUserDefaults
        //[dict removeObjectForKey:key];
        [dict setObject@"" forKey:key];
    }
}

Upvotes: 21

Related Questions