Friday, March 12, 2010

Revisited: Storing and retrieving information using plists

Quite a while back I wrote a post about storing and retrieving information using plists. This has turned out to be one of the most appreciated posts in my blog, both by the number of readers and the number of comments. My post also contained some errors, but friendly commenters have helped eachother out in order to resolve those errors.

When I wrote the original post I hadn't really resolved how to store information on a real device in the proper way. For example, if you have a file in your application bundle which you want to update, you should start by copying the file from the bundle to the Documents-directory.

To resolve the issues from the first post and show the "proper way" of handling files that are updated by the application I simply created a new Window-based Application and edited the "applicationDidFinishLaunching" method to look like this:

- (void)applicationDidFinishLaunching:(UIApplication *)application {

// Override point for customization after application launch
[window makeKeyAndVisible];


// get the path to the "Documents" directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];

// get the path to our plist ("Documents/foo.plist")
NSString *plistPath = [documentsDirectory stringByAppendingPathComponent:@"foo.plist"];

// read or create plist

NSMutableDictionary *dict;
// check if our plist already exists in the Documents directory...
NSFileManager *fileManager = [NSFileManager defaultManager];
if ( [fileManager fileExistsAtPath:plistPath] ) {
// ...if it does, read it
NSLog(@"dict existed, reading %@", plistPath);
dict = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
} else {
// ...if it doesn't, create it
NSLog(@"dict didn't exist, creating...");
dict = [NSMutableDictionary dictionaryWithCapacity:1];

// Fill the dictionary with default values, either by copying
// a default plist from our bundle to the Documents directory
// or simply creating a new dictionary and writing it to th
// Documents directory. Here we choose to create a new
// dictionary rather than providing a default plist in the bundle.

// create a NSNumber object containing the
// integer value 1 and add it as 'key1' to the dictionary.
NSNumber *number = [NSNumber numberWithInt:1];
[dict setObject:number forKey:@"key1"];

// write dictionary to Documents directory...
NSLog(@"writing to %@...", plistPath);
[dict writeToFile:plistPath atomically:YES];
}

// dump the contents of the dictionary to the console
NSLog(@"dumping...");
for (id key in dict) {
NSLog(@"key=%@, value=%@", key, [dict objectForKey:key]);
}

// check if key2 is present
NSString *value2 = (NSString *)[dict valueForKey:@"key2"];
if ( value2 == nil ) {
NSLog(@"key2 didn't exist, adding...");
[dict setObject:@"default-2" forKey:@"key2"];
}

// dump the contents of the dictionary to the console
NSLog(@"dumping...");
for (id key in dict) {
NSLog(@"key=%@, value=%@", key, [dict objectForKey:key]);
}
}

The code should be rather self-explanatory thanks to the inline comments, but I really recommend you to read my first post on this subject to really understand what is going on.

A thing worth commenting on is the really strange path names logged to the console. These pathnames can look like this:

2010-03-12 09:09:15.557 Plist2[10780:20b] writing to /Users/henrik/Library/Application Support/iPhone Simulator/User/Applications/E10D7186-1219-4C52-804A-FE217F760967/Documents/foo.plist...

This path is a path in the Mac OS filesystem used by the iPhone simulator when testing an application. The strange part of it is the long string of hexadecimal numbers starting with E10D7. This is the so called GUID which is unique for each installed application. A lot has been written about this in other sources on the Internet, so I won't go further into that:





One annoying thing about it and that is that it changes each time you recompile your application. That means, that each time you build and test your application the files created by the application will be somewhere else in the Mac OS filesystem. This makes it hard to inspect the files written by your application. Fortunately, modern versions of XCode copy the contents from the "old path" to the "new path" so even though the GUID changes, the files created by the application the last time are available the next time you build and test it.

If you couple this annoying thing with the fact that nothing is logged to the console if you exit your application by pressing the home-button in the simulator and then relaunch the application by pressing its icon in the simulator. This means that if you want to see what is logged to the console you are forced to relaunch the application from XCode, which will create a new GUID and thus a new path in the Mac OS filesystem.

However, since XCode nowadays copies the contents from the old to the new path it is possible to test how the application responds to "external changes" such as removing a file stored by the application, etc. This can be very convenient to perform effecient testing of an application.

For the application in this post you can test how it behaves if "foo.plist" is present or not by first running the application from XCode (CMD-Return), observing which path is logged to console, removing that file and then re-running the application from XCode (CMD-Return). This will give you something like this on the console:

[Session started at 2010-03-12 10:18:24 +0100.]
2010-03-12 10:18:27.456 Plist2[10888:20b] dict existed, reading /Users/henrik/Library/Application Support/iPhone Simulator/User/Applications/DB3B1A75-BF08-461E-919A-DBFFCC994036/Documents/foo.plist
2010-03-12 10:18:27.464 Plist2[10888:20b] dumping...
2010-03-12 10:18:27.468 Plist2[10888:20b] key=key1, value=1
2010-03-12 10:18:27.469 Plist2[10888:20b] key2 didn't exist, adding...
2010-03-12 10:18:27.471 Plist2[10888:20b] dumping...
2010-03-12 10:18:27.475 Plist2[10888:20b] key=key1, value=1
2010-03-12 10:18:27.477 Plist2[10888:20b] key=key2, value=default-2

[Session started at 2010-03-12 10:19:38 +0100.]
2010-03-12 10:19:40.159 Plist2[10897:20b] dict didn't exist, creating...
2010-03-12 10:19:40.180 Plist2[10897:20b] writing to /Users/henrik/Library/Application Support/iPhone Simulator/User/Applications/5B1E8FF3-45D6-443B-BB0B-2073EAE22520/Documents/foo.plist...
2010-03-12 10:19:40.191 Plist2[10897:20b] dumping...
2010-03-12 10:19:40.198 Plist2[10897:20b] key=key1, value=1
2010-03-12 10:19:40.199 Plist2[10897:20b] key2 didn't exist, adding...
2010-03-12 10:19:40.199 Plist2[10897:20b] dumping...
2010-03-12 10:19:40.200 Plist2[10897:20b] key=key1, value=1
2010-03-12 10:19:40.202 Plist2[10897:20b] key=key2, value=default-2

3 comments:

  1. Great stuff - keep these coming, I find them very useful.

    ReplyDelete
  2. Hi Henrick!

    This is great. I have pieced this together from a variety of sources, but it is nice to see so well stated and all in one place.

    I found this post because I am trying to solve a problem with my own PList implementation, and thought you might have some insight.

    I have a high scores table in my PList, which is a Dictionary of Dictionaries like this:
    HIGH SCORES
    EASY
    -> score
    -> score
    NORMAL
    -> score
    HARD
    -> score

    I want to allow users to clear the high scores. To do this I just flush the entire dictionary at each level (highScores setValue: forKey:@"EASY"). But that is where I get into trouble.

    When I add new scores to the dictionary, the scores are getting saved as strings rather than numbers (which is how the implementation handles them).

    I am setting values like this:
    [scoresForDifficulty setValue:[NSNumber numberWithInt:numMoves] forKey:dateString];

    So the value is of type NSNumber when it goes into the dictionary. But when I save the outermost dictionary with:
    [settingsContent writeToFile:path atomically:YES];
    Where settingsContent is the root of the PList, it stores the score values as Strings.

    Am I doing something wrong, not doing something I should, or did I find an implementation issue in storing PLists in this way?

    Cheers,

    Chris

    ReplyDelete
  3. Hello Henrik. I have followed your previous and this post to do this task, but nothing works in my Xcode. Is there more codes I should write in order to make this Xcode project function? Perhaps something in .h file? Please answer asap, thank you. Mar

    ReplyDelete