Implementing Object Archiving
A
running iPhone application has a vast number of objects in memory.
These objects are interlinked to one another with references in such a
way that if you were to visualize the relationships, they would appear
like a tangled spider web. This web of all the objects in an application
and all the references between the objects is called an object graph.
A running iPhone application
is not much more than the program itself (which is always the same, at
least until the user installs an update) and the unique object graph
that is the result of all the activity that has occurred in the running
of the application up to that point. One approach to storing an
application’s data (so that it is available when the application is
launched again in the future) is to take the object graph, or a subset
of it, and store the object graph on the file system. The next time the
program runs, it can read the graph of objects from the file system back
into memory and pick up where it left off, executing the same program
with the same object graph.
Most object-oriented
development environments have a serialization mechanism that is used to
stream a graph of objects out of memory and onto a file system and then
back into memory again at a later time. Object archiving is the Cocoa
version of this process. There are two main parts to object archiving: NSCoder and NSCoding. An NSCoder object can archive (encode and decode) any object that conforms to the NSCoding protocol. Apple supplies NSCoder for most data types, and any custom objects we want to archive implement the NSCoding protocol. We are in luck because the NSCoding protocol consists of just two methods: initWithCoder and encodeWithCoder.
Let’s start with encodeWithCoder. The purpose of encodeWithCoder is to encode all the instance variables of an object that should be stored during archival. To implement encodeWithCoder,
decide which instance variables will be encoded and which instance
variables, if any, will be transient (not encoded). Each instance
variable you encode must be a scalar type (a number) or must be an
object that implements NSCoding.
This means all the instance variables you’re likely to have in your
custom objects can be encoded because the vast majority of Cocoa Touch
and Core Foundation objects implement NSCoding. On the iPhone, NSCoder uses keyed encoding, so you provide a key for each instance variable you encode. The encoding for our FlashCard class is shown in Listing 14.
Listing 14.
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.question forKey:kQuestion]; [encoder encodeObject:self.answer forKey:kAnswer]; [encoder encodeInt:self.rightCount forKey:kRightCount]; [encoder encodeInt:self.wrongCount forKey:kWrongCount];
}
|
Notice that we used the encodeObject:forKey method for NSStrings and you’d use the same for any other objects. For integers, we used encodeInt:forKey. You can check the API reference documentation of NSCoder for the complete list, but a few others you should be familiar with are encodeBool:forKey and encodeDouble:forKey and encodeBytes:forKey. You’ll need these for dealing with Booleans, floating-point numbers, and data.
The opposite of encoding is decoding, and for that part of the protocol there is the initWithCoder method. Like encodeWithCoder, initWithCoder is keyed, but rather than providing NSCoder an instance variable for a key, you provide a key and are returned an instance variable. For our FlashCard class, decoding should be implemented, as in Listing 15.
Listing 15.
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) { self.question = [decoder decodeObjectForKey:kQuestion]; self.answer = [decoder decodeObjectForKey:kAnswer]; self.rightCount = [decoder decodeIntForKey:kRightCount]; self.wrongCount = [decoder decodeIntForKey:kWrongCount]; } return self;
}
|
The four keys we used should be defined as constants in the FlashCard.h header file. Add them now:
#define kQuestion @"Question"
#define kAnswer @"Answer"
#define kRightCount @"RightCount"
#define kWrongCount @"WrongCount"
The last step is to update the class definition in the FlashCard.h header file to indicate that FlashCard implements the NSCoding protocol:
@interface FlashCard : NSObject <NSCoding> {
Our FlashCard class is now archivable, and an object graph that includes FlashCard object instances can be persisted to and from the file system using object archiving.
Archiving in the Flash Cards
To fix the fatal flaw in the FlashCards application, we need to store all the flash cards on the file system. Now, because FlashCard implements the NSCoding
protocol, each individual flash card is archivable. Remember that
object archiving is based on the notion of storing an object graph and
we are not looking to store each flash card in an individual file
(although we certainly could if we wanted to). We want one object graph
with references to all of our flash cards so that we can archive it into
a single file.
It turns out that the FlashCards application already has such an object graph in memory in the form of the FlashCardsViewController’s NSMuteableArray property called flashCards. The flashCards
array has a reference to every flash card the user has defined, so it
forms the root of an object graph that contains all the flash cards. An NSMuteableArray, like all the Cocoa data structures, implements NSCoding, so we have a ready-made solution for archiving an object graph containing all the flash cards.
We need a location for the
file that’ll store the flash cards. We’d like the flash cards to be
safely backed up each time the user syncs her device with iTunes, so
we’ll put the file in the application’s Documents directory. We can call
the file anything; object archiving doesn’t put any restrictions on the
filename or extension. Let’s call it FlashCards.dat.
We’ll need the full path to this file both when we store the flash
cards to the file system and when we read them from the file system, so
let’s write a simple helper function that returns the path to the file.
Open the FlashCardsViewController.m file in the Classes group, and add
the method in Listing 16.
Listing 16.
-(NSString *)archivePath { NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex: 0]; return [docDir stringByAppendingPathComponent:@"FlashCards.dat"]; }
|
We need to archive the
array of flash cards to the file before the application moves to the
background and then unarchive the array of flash cards from the file
when the application is loaded. To write an archive to a file, use the archiveRootObject:toFile method of NSKeyedArchiver. We’ll want to do this when the FlashCardsAppDelegate receives the applicationDidEnterBackground event, so define a method in the FlashCardsViewController.m file that FlashCardsAppDelegate can call to archive the flash cards:
-(void)archiveFlashCards {
[NSKeyedArchiver archiveRootObject:flashCards toFile:[self archivePath]];
}
Add the new method to the FlashCardsViewController.h file:
-(void)archiveFlashCards;
Open the FlashCardsAppDelegate.m file and update applicationDidEnterBackground so it will call the archiveFlashCards method of the FlashCardsViewController:
- (void)applicationDidEnterBackground:(UIApplication *)application {
[viewController archiveFlashCards];
}
Each time our application
enters the background, whatever flash cards are in the array will be
written out to the FlashCards.dat file in the Documents directory. On
startup, we need to read the archive of the array from the file. To
unarchive an object graph, use the unarchiveObjectWithFile method of NSKeyedUnarchiver. It’s possible this is the first time the application has ever been run, and there won’t yet be a FlashCards.dat file. In this case, unarchiveObjectWithFile returns nil and we can simply create a new, empty array like we did before the FlashCards application had data persistence. Update the viewDidLoad method of the FlashCardsViewController.m file as follows:
- (void)viewDidLoad {
self.flashCards = [NSKeyedUnarchiver
unarchiveObjectWithFile:[self archivePath]];
self.currentCardCounter = -1;
if (self.flashCards == nil) {
self.flashCards = [[NSMutableArray alloc] init];
}
[self showNextCard];
[super viewDidLoad];
}
That’s all there is to it. When the model objects of an application all implement NSCoding,
object archiving is a simple and easy process. With just a few lines of
code, we were able to persist the flash cards to the file system. Run
the application and give it a try!
You may notice that, if you’re
running under iOS 4.x or later, you’ll need to force the application to
quit using the iOS Task Manager before you can fully test that data
archiving is working. This is because iOS 4 doesn’t quit your
application; it suspends it and moves it to the background!