(overly simplistic) saving state in oF for iPhone

save_stateThere was a recent comment about saving / restoring application state when using openFrameworks for iPhone which got me to thinking about how to do it. Apple’s frameworks provide a fairly thorough way to save state to the disk and restore later. There seem to be three primary ways to do this: simple plist files (usually encoded in binary on the iPhone), archived data (they like to refer to this as freeze-dried object graphs) and core data.

I believe that archiving objects require methods inherited from NSObject, which we don’t have in openFrameworks’ ofSimpleApp. Core Data seems like overkill, so I looked into using plist files.

There are likely better ways to do this, but this ad-hoc solution works wonderfully for a small app I’m working on, and only requires a bit of Objective-C code that could likely be moved up into a nice wrapper class. However, since the question was asked I’d just like to get it out there before working on a more elegant approach.

The easiest way to store state is with NSUserDefaults. The following returns a pointer to the shared user defaults object then writes several values to it. Adding data to the object will get cached in memory and eventually written to disk (or upon receiving a synchronize message). The key is just a NSString, which can be arbitrarily named, but you’ll need to use identical values to retrieve the data later.

NSUserDefaults *savedData = [NSUserDefaults standardUserDefaults];

[savedData setInteger:score forKey:@"score"];
[savedData setInteger:pointsAvail forKey:@"pointsAvail"];
[savedData setInteger:timeRemaining forKey:@"time"];
[savedData synchronize];

The savedData object should be autoreleased. Plists can handle basic data types such as integer, float, bool and double. Look at the documentation for specific messages, but it’s usually like setFloat:forKey: or setBool:forKey:. It can also handle some NSObjects such as NSString, NSArray and NSDictionary. The selector for those is setObject:forKey:.

Reading back is just as simple. If a key doesn’t exist 0 is returned for a number, and nil for other strings, arrays and dictionaries. In the below example score, pointsAvail and timeRemaining are instance variables of testApp.

NSUserDefaults *savedData = [NSUserDefaults standardUserDefaults];

score = [savedData integerForKey:@"score"];
pointsAvail = [savedData integerForKey:@"pointsAvail"];
timeRemaining = [savedData integerForKey:@"time"];

To get this all working as expected, retrieve the defaults in setup(), and be sure to do some sanity checking if the values aren’t found (ie. if this is the first run of the app). Because we’re mixing Objective-C and C++ in testApp, be sure to rename main.cpp to main.mm, testApp.cpp to testApp.mm and add “#import <UIKit/UIKit.h>” to the top of testApp.mm.

A code snippet from setup():

// test for previous state
NSUserDefaults *savedData = [NSUserDefaults standardUserDefaults];
int hideInstructions = [savedData integerForKey:@"hideInstructions"];

// if the key "hideInstructions" doesn't exist, then it will return 0
if(hideInstructions) {
// we've must have run at least once before and written "hideInstructions"
// restoreGame will read in the rest of the saved values:
} else {
// assume first run
[savedData setInteger:1 forKey:@"hideInstructions"];

openFrameworks supports an exit() method which gets called dutifully on the iPhone when the application will quit. This is a fine place to write out data to the userDefaults. The challenge as a programmer will be to determine the appropriate data to save and restore. In my simple game, I needed to save the game state for score and time, but also the position and basic state of the widgets on screen. With a larger game I’d look into writing methods into my objects cough up their own important data and then request it when the app is exiting. Keep in mind that there is a narrow window of time that the iPhone OS provides between receiving a signal to quit and when it will forcefully quit your app (like a few seconds) so saving data needs to happen quickly.

This method is purposefully rudimentary, but I think it works well for this particular use.






Leave a Reply