There are several ways to store data for Cocoa applications. You could implement your own file loading and saving methods and store the data in any number of formats you want, but it’s time-consuming. You could use Cocoa’s built-in database functionality, called Core Data, but that’s not ideal for everything and it’s very hands-off in its approach.
If you decide to store your data outside of a database, it would be great if Cocoa provided a means to easily manage files and get your data into them, without having to do any of the low-level stuff.
That’s where the NSKeyedArchiver comes in.
Archiving and unarchiving data
When Cocoa refers to archiving data, it doesn’t mean making a compressed zip file, it means putting all the relevant data together and then saving it to a single file. These files have a certain structure that Cocoa can then unarchive. The archiving takes place via the NSKeyedArchiver object, and is read back via the (you guess it!) NSKeyedUnarchiver.
The way NSKeyedArchiver works is by taking a “root” object. This object will know what information needs to be stored, and “tells” the NSKeyedArchiver what it is and how to do it. The information is stored using string keys so that it can be retrieved using the same keys.
Now, the root object could be an array (like it is with my Todo app) or it could be an object that contains an array and links to other objects. The thing all of these have in common is that must know how to give the NSKeyedArchiver the correct information to do its job.
They do this by conforming to the NSCoding protocol.
Saving data to a file
The first thing that needs to happen when attempting to save our objects is to implement the method in the NSCoding protocol that our coder (NSKeyedArchiver) expects to find, encodeWithCoder:
Here it is for our base object, TaskStore.
- (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:taskStore forKey:@"taskStore"]; }
This is pretty simple, but our TaskStore object only has one property, taskStore, which is an array of our root groups (sections).
As you can see, all we need to do encode our array is call encodeObject:forKey: on our coder object. Arrays conform to the NSCoding protocol, so they know how to encode themselves; we don’t need to iterate through arrays, but they do assume that the objects contained in them conform to NSCoding as well.
Here is the encodeWithCoder: method for our TaskGroup object:
- (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:name forKey:@"name"]; [coder encodeObject:subgroups forKey:@"subgroups"]; [coder encodeObject:tasks forKey:@"tasks"]; [coder encodeBool:expanded forKey:@"expanded"]; }
The difference here is that TaskGroup is more complex, with a number of different properties: an NSString, two NSMutableArrays, and a BOOL. The string and arrays can both call encodeObject:forKey: because they are Cocoa objects that conform to NSCoding.
The boolean, on the other hand, is a primitive C type and needs to be handled separately, which is why it has it’s own method (encodeBool:forKey:) on the coder. You would need to call any of the relevant methods for the other C types as well, like int or float.
I will leave the method of the tasks themselves up to your imagination.
Now that our objects know how to encode themselves, we need to get our NSKeyedArchiver to do so. We’ll implement the relevant methods on our TaskStore because it’s the root object. Once it initiates the load, it can read in the rest of the object tree.
First we’ll have a method that gives us a standard file location. This should probably be externalised to a settings file, but for such a simple project we’ll hardcode the file location (in the user Library).
- (NSString *) pathForDataFile { NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *folder = LIBRARY_PATH; folder = [folder stringByExpandingTildeInPath]; if ([fileManager fileExistsAtPath:folder] == NO) { [fileManager createDirectoryAtPath:folder withIntermediateDirectories:NO attributes:nil error:nil]; } NSString *fileName = @"WhatNow.taskStore"; return [folder stringByAppendingPathComponent: fileName]; }
All this does is check to see if the our program folder (LIBRARY_PATH, a constant declared at the beginning of the file) exists, creates it if it doesn’t, and then returns a string to the file including the full folder path.
We then need a method to use NSKeyedArchiver to save it to disk:
- (void)saveDataToDisk { NSString * path = [self pathForDataFile]; NSMutableDictionary *rootObject; rootObject = [NSMutableDictionary dictionary]; [rootObject setValue:[self taskStore] forKey:@"taskStore"]; [NSKeyedArchiver archiveRootObject:rootObject toFile: path]; }
The method we call on NSKeyedArchiver is a class method (there’s no reason to create an object from it). We first have to put our array into an NSMutableDictionary, but we could also put in other objects that are separate from our TaskStore if we wanted. If this were a document-based application, we might have document-specific settings, for example.
Anyway, this method is called from our Application Delegate whenever the app is closed. Because this is not a document-based app, auto-saving will need to be implemented separately.
That’s it for saving data. Our saveDataToDisk: method tells the shared NSKeyedArchiver to start saving data on our root array. The array iterates through the groups and tells them to encode. The groups tell their subgroups to encode, and all of the leaf groups tell the tasks to encode.
Now that saving works, we need to go the other way and rebuild the object tree from the file.
Reading data from a file
When we make our objects conform to NSCoding, they need to know how to initialise themselves not only when being created from scratch but also when being initialised from the file via NSKeyedUnarchiver. To do this, they need a new initialisation method called initWithCoder:
Here is the method for our base object, TaskStore:
- (id)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { taskStore = [coder decodeObjectForKey:@"taskStore"]; } return self; }
This one is quite simple and is basically the reverse of what we did to encode it. decodeObjectForKey: will return, in this instance, an array and we simply assign it to our instance variable. If we decoded the wrong key, it would raise an exception.
Our TaskGroup object is a little more complicated, but not by a whole lot:
- (id)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { [self setName:[coder decodeObjectForKey:@"name"]]; [self setSubgroups:[coder decodeObjectForKey:@"subgroups"]]; [self setTasks:[coder decodeObjectForKey:@"tasks"]]; [self setExpanded:[coder decodeBoolForKey:@"expanded"]]; } return self; }
Same as for the root object. Again, the boolean has its own method, decodeBoolForKey: because it is a primitive type. In this case we use the accessor method to assign the values because I’ve only set them up as properties, not with instance variable as well.
As with encoding, I’ll leave decoding the task to your imagination.
We also need to tell NSKeyedUnarchiver to get the data for us:
- (void)loadDataFromDisk { NSString * path = [self pathForDataFile]; NSDictionary * rootObject; rootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; [self setTaskStore:[rootObject valueForKey:@"taskStore"]]; }
We call this method when the application starts.
And that’s it for saving and loading to a file. The NSKeyedUnarchiver will tell the root object to decode from the file. The root object will unarchive all of it’s objects (in the array), which will then unarchive its object, ending up with exactly the same object tree that was saved.