views:

544

answers:

3

I'm storing a bunch of data in a .plist file (in the application documents folder), and it's structured like this:

Dictionary {
    "description" = "String Value",
    "sections" = Array (
        Array (
            Number,
            ...
            Number
        ),
        Array (
            Number,
            ...
            Number
        )
    ),
    "items" = Array (
        Array (
            Number,
            ...
            Number
        ),
        Array (
            Number,
            ...
            Number
        )
    )
}

If I just retrieve it with
NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithContentsOfFile:plistFile] I won't be able to replace the number objects, correct? So I'm recursing through the data right now and forming a mutable version of the whole thing, and it worked in one instance, but now it's telling me mutating method sent to immutable object when the whole thing is mutable.

Is there an easier/better way to do this? If it makes a difference, my data is just integers and booleans.

+6  A: 

I usually find it easier to create one or more custom classes to handle loading and saving. This lets you convert the arrays to mutableArrays explicitly:

MyThing.h

@interface MyThing : NSObject
{
    NSString * description;
    NSMutableArray * sections;
    NSMutableArray * items;
}
@property (copy) NSString * description;
@property (readonly) NSMutableArray * sections;
@property (readonly) NSMutableArray * items;
- (void)loadFromFile:(NSString *)path;
- (void)saveToFile:(NSString *)path;
@end

MyThing.m

@implementation MyThing
@synthesize description;
@synthesize sections
@synthesize items;

- (id)init {
    if ((self = [super init]) == nil) { return nil; }
    sections = [[NSMutableArray alloc] initWithCapacity:0];
    items = [[NSMutableArray alloc] initWithCapacity:0];
    return self;
}

- (void)dealloc {
    [items release];
    [sections release];
}

- (void)loadFromFile:(NSString *)path {
    NSDictionary * dict = [NSDictionary dictionaryWithContentsOfFile:path];
    [self setDescription:[dict objectForKey:@"description"]];
    [sections removeAllObjects];
    [sections addObjectsFromArray:[dict objectForKey:@"sections"]];
    [items removeAllObjects];
    [items addObjectsFromArray:[dict objectForKey:@"items"]]; 
}

- (void)saveToFile:(NSString *)path {
    NSDictionary * dict = [NSDictionary dictionaryWithObjectsAndKeys:
                           description, @"description",
                           sections, @"sections",
                           items, @"items",
                           nil];
    [dict writeToFile:path atomically:YES];
}

@end;

With that done, you can encapsulate all of the packaging and unpackaging code in your loadFromFile and saveToFile methods. The major benefit of this approach is that your main program gets a lot simpler, and it allows you to access the elements of your data structure as properties:

MyThing * thing = [[MyThing alloc] init];
[thing loadFromFile:@"..."];
...
thing.description = @"new description";
[thing.sections addObject:someObject];
[thing.items removeObjectAtIndex:4];
...
[thing saveToFile:@"..."];
[thing release];
e.James
Wow, that's quite the thorough answer, sir :)That makes so much sense I have no idea why it hadn't crossed my mind.Thanks a bunch!
Kelso.b
+1  A: 

What you want is a deep mutable copy. Cocoa doesn't include a way to do it. A few people have written such deep-copy implementations before (example).

However, Core Foundation includes the CFPropertyList API, which does have support both for creating deep mutable copies of property list objects as well as reading in property lists from disk as mutable datatypes. (And, of course, Core Foundation's property list types are toll-free bridged with Cocoa's, meaning you don't have to convert between them — an NSArray is a CFArray and vice-versa.)

Chuck
+2  A: 

Instead of writing all that custom class junk, you should use NSPropertyListSerialization. Specifically, see the propertyListWithData:options:format:error: method. Example usage:

NSMutableDictionary *d = [NSPropertyListSerialization propertyListWithData:[NSData dataWithContentsOfFile:@"path/to/file"] 
                                                                   options:NSPropertyListMutableContainers
                                                                    format:NULL
                                                                     error:NULL];

This will make all the containers mutable, but keep the leaf nodes (e.g. NSStrings) immutable. There's also an option to make the leaves mutable too.

Colin Barrett
+1 That is intriguing. Apple's documentation states that the `options` parameter is not to be used when reading (as you show here), but it can, in fact be used when *writing* the plist file, so that the data, when read, is mutable. See http://developer.apple.com/mac/library/documentation/Cocoa/Reference/Foundation/Classes/NSPropertyListSerialization_Class/Reference/Reference.html
e.James
I would still argue for a custom class in order to get the OO benefits of property accessors and to encapsulate the read and write methods, but this technique will definitely help for those occasions when a simple read-modify-write is in order. Great stuff!
e.James
Yep, I went the custom class route for convenience, but this looks fairly straightforward. Thanks for the input.
Kelso.b
The documentation is wrong and *very* confusing. Ignore it. Read the header file (NSPropertyList.h) instead, which say that in the two methods that take NSPropertyListWriteOptions arguments, it's ignored and currently set to zero. Sigh.
Colin Barrett