What's the deal with .plists?
June 30, 2008 1:43 PM
In the last entry we talked about one way to abstract away the differences between ALife models (specifically, differences in relevant statistics) by connecting classes with key-value observation. However, the solution was a little constricting and nonextensible.
In particular, the way we coded which statistics an ALife model would support was a little brittle. We used a simple NSDictionary, with keys corresponding to the keypath of a statistics collector object, and the value as a human-readable title. Though this works for the current implementation, it also means that there is no way to add other configuration options: for example, most of the statistics that we'll collect will be single values, while some will be sets of values; with our simple dictionary-based definition of statistics, there is no good way to differentiate between these or other options.
To resolve this impasse, we could just make the dictionary more complicated. In particular, we might want each entry in the dictionary to be, instead of a simple title, another dictionary object containing key-value pairs for information like the title, key path or other information.
In fact, this is actually what CocoaBugs does. However, doing this in code gets pretty ugly: Cocoa doesn't have simple dictionary literals like scripting languages, and maintaining large dictionaries of data like that gets messy fast. Where in Ruby we could say something like,
statistics = {:population => {:title => "Population",
:key_path => "population",
:singular => true},
:birth_rate => { ... }}
in Cocoa it gets a little uglier:
NSMutableDictionary *statisticsDescriptors = [NSMutableDictionary dictionary];
// add the population entry
[statisticsDescriptors addObject:[NSDictionary dictionaryWithObjectsAndKeys:
@"Population", @"title",
@"population", @"keyPath",
[NSValue valueWithBool:YES], @"singular", nil]];
// add the birth rate entry
...
That's just no good at all.
Luckily, Cocoa provides some nice frameworks for allowing us to define structured data like this in a more maintainable way: property lists, more commonly known by their file extension, .plist.
A plist (pronounced, as far as I know, as "pea-list") is an XML file which describes keyed data according to a simple, well-defined syntax. The iTunes library, for example, is a .plist, storing information about songs and playlists; most preference files in Mac OS X are .plists.
In our case, we're going to build a plist which will describe the statistics values for an ALife model, and which we can later extend to encompass the more complicated parameters an artificial life model might accept.
In this case, I'll be using some existing code for Conway's Game of Life to illustrate this technique. Thanks to the object-oriented nature of Cocoa and CocoaBugs, it was straightforward to drop this into the CocoaBugs codebase. (There will be more on the specifics of this in a later post.)
I created a new StatisticsOfLife class in the Game of Life bundle which acts like the WorldStatistics class in the Packard Bugs code discussed previously. It listens for changes to the "generation" property of the GameOfLife object, and updates its own statistics properties every generation. Here I'll just be collecting population statistics.
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
{
if ([keyPath isEqual:@"generation"]) {
[self updateStatistics];
}
}
- (void)updateStatistics;
{
NSLog(@"Collating statistics...");
// population
int pop = 0;
for (NSMutableArray *row in game.grid) {
for (CellOfLife *cell in row) {
if (cell.alive)
pop++;
}
}
self.population = [NSSet setWithObject:[NSNumber numberWithInt:pop]];
}
(Note that I'm still using NSSets to store all statistics data---we'll fix that later on.)
In the WorldStatistics class, we defined a "descriptions" method so our app controller could tie the model to the CocoaBugs statistics controller. Now, we can use a .plist to do the same job.
Using Apple's Property List Editor app, it's simple to make such a plist:
![]()
This translates into the relatively readable XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>statistics</key>
<dict>
<key>population</key>
<dict>
<key>keyPath</key>
<string>population</string>
<key>title</key>
<string>Population</string>
<key>singular</key>
<true/>
</dict>
</dict>
</dict>
</plist>
Now in the GameOfLife controller class, ControllerOfLife, we'll add a bit to the init method to load the contents of this file into an NSDictionary:
NSString *thePath = [[NSBundle mainBundle] pathForResource:@"GameOfLife" ofType:@"plist"];
properties = [[NSDictionary dictionaryWithContentsOfFile:thePath] retain];
This "properties" dictionary will store the configuration parameters for the GameOfLife model. As suggested by the screenshot, the data model looks something like this:
statistics:
population:
keyPath: "population"
title: "Population"
singular: YES
(Later, we'll probably add a "parameters" dictionary at the same level as "statistics" to store options for initializing the model.)
So, where we used to do something like this:
WorldStatistics.m:
- (NSDictionary *)descriptions;
{
return [NSDictionary dictionaryWithObjectsAndKeys:
@"Population", @"population",
@"Births (total)", @"births",
@"Deaths (total)", @"deaths",
@"Average age", @"averageAge",
@"Mortality age", @"mortalityAge",
@"Gene survival", @"geneSurvival", nil];
}
BugsController.m:
- (void)awakeFromNib;
{
...
// set up statistics
statistics = [[WorldStatistics alloc] initWithWorld:world];
statisticsController.source = statistics;
// get descriptions of collated statistics
NSDictionary *descriptions = [statistics descriptions];
for (NSString *key in descriptions) {
[statisticsController registerForPath:key
name:[descriptions objectForKey:key]];
}
...
}
we can now do this:
StatisticsController.m:
- (void)setSource:(id)statisticsCollector
forStatistics:(NSDictionary *)descriptions;
{
self.source = statisticsCollector;
for (NSDictionary *description in [descriptions allValues]) {
[self registerForPath:[description objectForKey:@"keyPath"]
name:[description objectForKey:@"title"]];
}
}
AppController.m:
[statisticsController setSource:[simulationController statisticsCollector]
forStatistics:[[simulationController properties]
objectForKey:@"statistics"]];
Now we can collect statistics on the Game of Life. Once we build-and-go, here's how the population grows on a 150 x 150 board initialized to the r-pentomino (1000 generations):

There is one major downside to implementing statistics in this way. Where before, we could maintain the statistics for a model in one file, here we need to maintain two files: GameOfLife.plist and StatisticsOfLife.m. If we add a statistics key to GameOfLife.plist, and don't implement a corresponding property in StatisticsOfLife, we'll get a key-value coding error.
However, the advantages of the solution more than outweigh this downside. Using .plists for configuring our statistcs gives us:
Less coupling. The StatisticsController never talks to the StatisticsOfLife directly; all it knows is that, based on the .plist, the class referenced by the ControllerOfLife as its statistics collector is key-value compliant for certain keys. (Though we'll never use this particular feature, this means that we could even use built-in Cocoa dictionaries as statistics collection objects, since they are key-value compliant.)
Future extensibility. If we want to be able to specify that certain statistics objects are of a certain numeric type, should be represented in a certain way, or should only be collected under certain circumstances, we can just add a key to the plist entry, and not have to worry about maintaining statistics information in code.
In the next post, we'll look at some more advanced refactoring using Cocoa protocols and bundles. How exciting!

Leave a comment