Saving objects as property lists

Previously I have saved objects in Cocoa using NSEncoding.  However, for my game I want to save the GameModel’s layout as an intelligible text file.

One way to do this is to save the object as a property list (or plist), which is simply an NSDictionary.  The result is an XML file.

My game has a GameModel class and this has an NSArray of GameElements. So to pull this off, I added two methods to each of these classes.  The first returns a dictionary which describes the layout of the object.  For GameElement, it looks like this:

-(NSDictionary*) layoutDictionary {
  // Returns keys and values of all the parts of the object 
  //    that need to be saved to define this game layout.
  // This only needs to be overridden by subclasses 
  //    if special structs need to be added (e.g. Point3D).
  // Apart from this, instead, subclasses should add to layoutKeys.
  NSDictionary* dict1 = [self dictionaryWithValuesForKeys:self.layoutKeys];
  NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithDictionary:dict1];
  dict[@"where"] = NSStringFromPoint3D(self.where);
  dict[@"class"] = NSStringFromClass([self class]);
  return dict;

It uses the key-value coding (KVC) method dictionaryWithValuesForKeys, which returns the values of the object’s own methods or variables.  I declare which properties are important to save in the layoutKeys variable, which I set up in the GameElement initialisation.

However… you can see there is some special handling of the where struct above.  The reason is that while an NSDictionary can happily have any object in it, if you want to write it out as a plist file, you must only have “plist objects” – NSData, NSString, NSNumber, NSDate, NSArray, NSDictionary and the like. If “where” was in the layoutKeys, the first line above would happily add an NSValue object containing the where struct’s data.  But you could not write this out.

It would be great if you could add custom plist objects.  Please let me know if you know how to do this.  In the absence of this, I have written functions to convert the struct into a plist object (an NSString), and back again, instead. For CGPoint these functions already exist.

The second function then creates an object from a dictionary, for example:

+(GameElement*) elementWithLayoutDictionary:(NSDictionary*)dict inModel:(GameModel*)model {
    // Creates an element, given the relevant piece of the game layout dictionary, and the model.
    // This needs to be overridden by subclasses
    Point3D where = Point3DFromNSString([dict valueForKey:@"where"]);
    GameElement* element = [[self class] elementNamed:dict[@"name"] at:where inModel:model];
    element.scale = [[dict valueForKey:@"scale"] floatValue];
    return element;

The GameModel also has layoutDictionary and modelWithLayoutDictionary: functions, which loop through the array of GameElements, e.g.

+(GameModel*) modelWithLayoutDictionary:(NSDictionary*)dict {
    int elementNumber = 1;
    while (NSDictionary* elementDict = [dict valueForKey:
                [NSString stringWithFormat:@"Element-%d", elementNumber]]) {
        [self addElement:[NSClassFromString(elementDict[@"class"]) 
                elementWithLayoutDictionary:elementDict inModel:self]];

(You can see that this implementation allows for subclassing of the GameElements.)

That’s basically it.  I then write this out to a plist file using
[layoutDict writeToFile:path atomically:YES];
and read it in using
[NSDictionary dictionaryWithContentsOfFile:path].

I suspect this could be improved:

  • KVC allows for a one-to-many relationship.  Can I use this to remove my custom handling of the array?
  • Is there a way to remove the custom handling of non-plist-objects like the Point3D struct?
  • Perhaps this all comes for free if I used CoreData… but do you then lose control of the data format?

Any thoughts?