Sometimes, ‘NO’ is just not enough. There are many great features about XCode, the LLVM compiler, Objective-C and the Cocoa Touch libraries but sometimes they just don’t give enough information. One such case is NSDictionary’s “-(BOOL)writeToURL:atomically:” method.
I was working on a project, and I just wanted to store some information to the documents directory.
NSDictionary *allData = [NSDictionary dictionaryWithObjectsAndKeys: imageArray, @"images", contactArray, @"contacts", profilesDict, @"profiles", nil]; [allData writeToURL:filename atomically:YES];and of course, writeToURL:, fails with a ‘NO’ value.
I scoured my data for invalid objects, or things that wouldn’t save correctly (i.e. not a valid plist value), but no luck. All my data looked correct and of course there’s no error code, NSError, or NSException associated with this, so I switched to the more fancy writer hoping to get something more clear, but NSPropertyListSerialization just gave me “Error Code: 3851” which is “NSPropertyListWriteStreamError“.
Not much more descriptive than ‘NO’…
So I wrote a simple category with a recursive method for NSObject that traverses the object graph and tells me what objects at runtime are compatible or not with a given NSPropertyListFormat (requires iOS 4 and Blocks). Pass ‘nil’ in the first time for ‘path’.
@implementation NSObject (DiscoverPLIST) - (void) discoverPlist:(NSPropertyListFormat)format andPath:(NSString *)path { if (![NSPropertyListSerialization propertyList:self isValidForFormat:format]) { NSLog(@"%@ - not valid for format: %d", path, format); } if ([self isEqual:[NSNull null]]) { NSLog(@"%@ = (NSNull*)%@", path, self); } else if ([self isKindOfClass:[NSArray class]]) { [(NSArray*)self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { NSString *newPath = [path stringByAppendingFormat:@"[%i]",idx]; [obj discoverPlist:format andPath:newPath]; }]; } else if ([self isKindOfClass:[NSDictionary class]]) { [(NSDictionary*)self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { NSString *newPath = [path stringByAppendingFormat:@".%@",key]; [obj discoverPlist:format andPath:newPath]; }]; } else if ([self isKindOfClass:[NSString class]]) { NSLog(@"%@ = (NSString*)%@", path, self); } else if ([self isKindOfClass:[NSNumber class]]) { NSLog(@"%@ = (NSNumber*)%@", path, self); } else if ([self isKindOfClass:[NSDate class]]) { NSLog(@"%@ = (NSDate*)%@", path, self); } else if ([self isKindOfClass:[NSData class]]) { NSLog(@"%@ = (NSData*)%@", path, [self description]); } else { NSLog(@"%@ = Invalid Class: %@", path, [self class]); } } @endAnd that told me the data objects in my tree where all correct! Except the root and “profilesDict” object – which confused me. So, I took things out of that dictionary and and put them into an array, and voila! Saved correctly.
So what was wrong? I was using NSNumber as a Key for values in “profilesDict” (customer IDs or something similar) Lesson Learned: Only NSStrings are valid Keys for saving to a property list, even though you can use NSNumber and even other custom objects as keys for NSDictionaries.