14

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?

Thomson Comer
  • 3,919
  • 3
  • 30
  • 32

3 Answers3

21

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"];
leviathan
  • 11,080
  • 5
  • 42
  • 40
21

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];
    }
}
Thomson Comer
  • 3,919
  • 3
  • 30
  • 32
  • 6
    Btw I hate you `JSONKit` and your !@#%@#! `NSNull`s – Thomson Comer Feb 24 '12 at 02:41
  • 1
    I determined that `[dict removeObjectForKey:key];` is actually an insufficient solution for my problem, because now missing values will never be overwritten in `NSUserDefaults`. Instead, I'm now saving an empty string when an `NSNull` is detected, as reflected in the answer. – Thomson Comer Feb 24 '12 at 23:29
-2

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.

Community
  • 1
  • 1
Berk
  • 1,289
  • 12
  • 16
  • Why should I use objectForKey by default? I think explicit casting is not a good practise, and should be avoided. Also my comment fixes the exact same problem, doesn't it? By down-voting my comment (if you did) you are disabling people to reach a working example. @Jaro – Berk Sep 11 '15 at 14:09
  • 1: it's not the answer to the question because converting a dictionary to a string is possible and fixes the problem in this case BUT that does not solves the problem of having objects inside a dictionary which results a Dictianary not being able to be saved in NSUserDefaults.. 2: implicit is a better design choice I think. My reason is to avoid this (many times unexpected) errors you had. I don't want to leave the type checking doodies for the NSUserDefaults. It's better to write the Archiver methods if you wan't to be so explicit. – Yaro Sep 13 '15 at 06:51
  • 1: If the main data is dictionary, converting it to a string is useless, not needed, bad programming. I don't say conversion needed. [NSUserDefaults dictionaryForKey:MY_KEY] doesn't converts data at all. But if you save the main data with a type and try to retrieve it with another type, than it it returns nil, as it never saved. The main problem is not that the data isn't saved, but it causes the exact same symptom. 2: Avoiding typo is the main issue of IDE and programmer itself. Programming design should include other concerns, such as unit test cost, cohesion, clear dependencies etc. – Berk Sep 14 '15 at 08:40
  • so did u load that dictionary as as string or not? As I tested it's possible. – Yaro Sep 24 '15 at 01:48
  • I tried again and it is not possible. You can see my test code and log below. Log: 2015-09-28 13:45:18.159 Test App[20772:1787919] stringForKey: (null) 2015-09-28 13:45:18.159 Test App[20772:1787919] dictionaryForKey: { obj = key; } Code: [[NSUserDefaults standardUserDefaults] setObject:@{@"obj":@"key"} forKey:@"KEY"]; [[NSUserDefaults standardUserDefaults] synchronize]; NSLog(@"stringForKey: %@", [[NSUserDefaults standardUserDefaults] stringForKey:@"KEY"]); NSLog(@"dictionaryForKey: %@", [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"KEY"]); – Berk Sep 28 '15 at 10:57
  • 1: So, in my answer I meant anyone who makes the same mistake, should be using dictionaryForKey. -> This fixes the problem. 2: So, data is actually saved, but symptom is the same, it returns null like it didn't save at all. -> This is why I think my answer points to same question. 3: All in all, I don't understand why my answer is down-voted. – Berk Sep 28 '15 at 11:03