Saturday, July 24, 2010

HelloWorld: Saving Application State for iPhone Apps

So now I urgently need to know how to persist the data for my iPhone app. Otherwise, every time the user exits, they have to start from zero. Let's forget about how much mucking my app will need to be able to reconstruct state at any moment. First, the HelloWorld.

So before anything, I Google the problem and the first information points to the NSUserDefaults class. Apparently, this class can do it all, though if you need more complex models you might use CoreData. And it looks like all I'll need to do is to have my objects conform to the NSCoding protocol if they're in an NSArray. Nice. I might be able to do that. Let's get started.

But on the off-chance that you can just call writeToFile in arrays, let's try that first:

NSArray *fred = [NSArray arrayWithContentsOfFile:@"myArray"];
NSLog(@"is he null %@", [fred description]);

So that worked perfectly! It's null. So now let's try:

NSArray *bob = [[[NSArray alloc] initWithObjects:@"one", @"two", @"three", nil] autorelease];
[bob writeToFile:@"myArray" atomically:YES];
NSLog(@"that seemed okay %d", [bob count]);

And now I run it a third time and it's:

2010-07-24 16:06:56.217 TestingPersistence001[4442:207] is he null (
2010-07-24 16:06:56.219 TestingPersistence001[4442:207] that seemed okay 3

Which basically means that if we wanted to, we could run with that for now!

So let's go a bit further because that only took about 5 minutes so far, and see if we can get it into this NSUserDefaultsClass. And then we have to see if we can somehow serialize a thing that is holding a CGPoint (for instance).

So first we try to retrieve the defaults, so we can make sure we're not getting faked out (as we did above):

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *bob = [defaults arrayForKey:@"bob"];
if (bob == nil)
NSLog(@"sorry it's nil");

Now I have no idea how to persist this thing. Hmmm... So first let's set the array. But wait! There's no setArray method. Whatever. It must use setObject and sort 'em out.

bob = [[[NSArray alloc] initWithObjects:@"one", @"two", @"three", nil] autorelease];
[defaults setObject:bob forKey:@"bob"];
NSLog(@"made it");

Okay, so putting these two together and running twice didn't work. It doesn't persist automatically. How does it, then? Let's try the synchronize method. Good, that worked. So this is the final HelloWorld code:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *bob = [defaults arrayForKey:@"bob"];
if (bob == nil)
NSLog(@"sorry it's nil");
NSLog(@"bob has %d things", [bob count]);

if (bob == nil) {
bob = [[[NSArray alloc] initWithObjects:@"one", @"two", @"three", nil] autorelease];
[defaults setObject:bob forKey:@"bob"];
[defaults synchronize];
NSLog(@"made it");

Great. Now we only have to make a custom object, put a CGFloat on it and we're done with most things.

Note: I'm oversimplifying and there's a bunch of stuff about domains on the save, and a bunch of stuff about property lists on the setObject. But let's finish this up first.


#import <Foundation/Foundation.h>

@interface TheObject : NSObject {
CGPoint point;

@property (nonatomic, assign) CGPoint point;


with standard .m file (with @synthesize!)

So we run this

TheObject *object = [[[TheObject alloc] init] autorelease];
object.point = CGPointMake(1024, 768);
[defaults setObject:object forKey:@"theObject"];

and the console says, 

-[NSUserDefaults setObject:forKey:]: Attempt to insert non-property value '<TheObject: 0x59351c0>' of class 'TheObject'.

That was fast and wrong! Sooo... how can we make it a property value. Before reading anything, let's make it conform to NSCoding. And that didn't work. At all. And reading about it, it has nothing to do with anything. This thing has to be a property list. I can't take it anymore. Brute force!

On TheObject class

- (NSDictionary*) getSerializablePiece {
NSArray *keys = [[[NSArray alloc] initWithObjects:@"point", nil] autorelease];
NSArray *objects = [[[NSArray alloc] initWithObjects:NSStringFromCGPoint(self.point), nil] autorelease];
return [[[NSDictionary alloc] initWithObjects:objects forKeys:keys] autorelease];

And in the testing main

TheObject *object = [defaults objectForKey:@"theObj"];
if (object != nil) {
NSDictionary *dic = (NSDictionary*)object;
NSLog(@"what %@", [dic objectForKey:@"point"]);
object = [[[TheObject alloc] init] autorelease];
object.point = CGPointMake(1024, 768);
[defaults setObject:[object getSerializablePiece]  forKey:@"theObj"];
[defaults synchronize];

man is that ugly. But it does work. Now that we're done with the HelloWorld, it's time to sweep the floor and figure out how to use this stuff for real(s).

Conclusion: you can use NSUserDefaults, but it's a bit particular about what it will serialize.

No comments: