You can only serialize and deserialize
property list objects. Moreover, all of a property list object’s
constituent objects must also be property list objects. This limitation
hinders the usefulness of property lists. Therefore, rather than using a
property list, you can use archiving. Archiving is a more flexible
approach to persisting an object than a property list.
You create an archive using an NSKeyedArchiver. This
class persists any object that adopts the NSCoding protocol. You
reconstitute an object by using NSKeyedArchiver’s complement, the
NSKeyedUnarchiver class. In this section, you learn how to create a
class that adopts the NSCoding protocol. You then learn how to archive
and unarchive this class.
1. Protocols to Adopt
Archiving a class requires that a class adopt the
NSCoding protocol. The class should also adopt the NSCopying protocol if
you’re creating a class that adopts the NSCoding protocol.
NSCoding
Classes that adopt this protocol must implement the
encodeWithCoder: and initWithCoder: methods. The encodeWithCoder: method
encodes the object and the object’s instance variables so that they can
be archived.
-(void)encodeWithCoder:(NSCoder *)encoder
The initWithCoder: method decodes the object and the object’s instance variables.
-(id)initWithCoder:(NSCoder *)decoder
You use both methods in the example task that follows.
NSCopying
When implementing the NSCoding protocol, best
practices dictate that you also implement the NSCopying protocol.
Classes that implement the NSCopying protocol must implement the
copyWithZone method. Remember, when you set one object to another, you
are merely creating another reference to the same underlying physical
object. For instance, in the following code, both A and B are pointing
to the same Foo that was originally allocated and initialized by A.
Foo * A = [[Foo alloc] init];
Foo * B = A;
When you copy an object, you obtain a distinct
physical object, as if the object obtaining the copy actually allocated
and initialized the object.
Foo * A = [[Foo alloc] init];
Foo * B = [A copy];
The method that allows copying is the copyWithZone: method.
-(id)copyWithZone:(NSZone *)zone
You can use either this method or NSObject’s copy
method to obtain what is called a “deep copy” of an object. For more
information, refer to Apple’s “Memory Management Programming Guide for
Cocoa,” available online.
2. NSKeyedArchiver and NSKeyedUnarchiver
The NSKeyedArchiver class archives objects, while the NSKeyedUnarchiver class unarchives objects.
NSKeyedArchiver
NSKeyedArchiver stores one or more objects to an
archive using the initForWritingWith MutableData method. To be archived,
an object must implement the NSCoding protocol.
-(id)initForWritingWithMutableData:(NSMutableData *)data
This method takes a writable data object and returns the archived object as an id. You can then write the archive to disk.
The steps for creating and writing an archive to disk are as follows. First, create an NSMutableData object.
NSMutableData * theData = [NSMutableData data];
After creating the data object, create an NSKeyedArchiver, passing the newly created data object as a parameter.
NSKeyedArchiver * archiver = [[NSKeyedArchiver alloc]
initForWritingWithMutableData:theData];
After initializing the NSKeyedArchiver, encode the
objects to archive. If you wish, you can encode multiple objects using
the same archiver, provided all archived objects adopt the NSCoding
protocol. The following code snippet illustrates:
[archiver encodeObject:objectA forKey:@"a"];
[archiver encodeObject:objectB forKey:@"b"];
[archiver encodeObject:objectC forKey:@"c"];
[archiver finishEncoding];
After archiving, write the data object, which now contains the archived objects, to a file.
[theData writeToFile:"myfile.archive" atomically:YES]
NSKeyedUnarchiver
You use NSKeyedUnarchiver to unarchive an archive.
NSKeyedUnarchiver reconstitutes one or more objects from a data object
that was initialized with an archive. To be unarchived, an object must
implement the NSCoding protocol. When programming for iOS, you use the
initForReadingWithData: method.
-(id)initForReadingWithData:(NSData *)data
The steps to unarchive are similar to archiving. First, create an NSData object from the previously archived file.
NSData * theData =[NSData dataWithContentsOfFile:"myfile.archive"];
After creating the data object, create and initialize an NSKeyedUnarchiver instance.
NSKeyedUnarchiver * uarchiver = [[NSKeyedUnarchiver alloc] initForRead
ingWithData:theData];
After initializing the NSKeyedUnarchiver, unarchive the objects previously archived.
A * objA = [[unarchiver decodeObjectForKey:@"a"] retain];
B * objB = [[unarchiver decodeObjectForKey:@"b"] retain];
C * objC = [[unarchiver decodeObjectForKey:@"c"] retain];
[unarchiver finishDecoding];
[unarchiver release];
Create a new View-based Application called Encoding. Create a new Objective-C class called Foo. Add
two properties to Foo. Make one property an NSString and name it “name”
and make the other property an NSNumber and name it “age.” Have Foo adopt the NSCopying and NSCoding protocols (Listings 1 and 2). Remember, Foo must get deep copies of name and age. Modify Foo so that it implements the encodeWithCoder:, initWithCoder:, and copyWithZone: methods. Add Foo as a property to EncodingAppDelegate (Listings 3 and 4). Implement
the applicationWillTerminate: method and modify the
applicationDidFinish LaunchingWithOptions: method to decode and encode
Foo. If
you’re building for SDK 4.0 or later, then iOS will suspend rather than
terminate your application, so the applicationWillTerminate will never
be called. Edit Encoding-Info.plist in Resources and add another value
to the end of the plist with key UIApplicationExitsOnSuspend, type
Boolean and value YES. (Later we’ll talk about using archiving to save
your application’s state on suspension, so that it can resume where it
left off whether iOS terminates it or only suspends it.) Click
Run and the debugging log will indicate that it’s the first pass
through. Stop execution and then Run again and the debugging log will
indicate that you’ve unarchived the Foo object.
|
Listing 1. Foo.h
#import <Foundation/Foundation.h>
@interface Foo : NSObject <NSCoding, NSCopying> {
NSString * name;
NSNumber * age;
}
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSNumber * age;
@end
|
Listing 2. Foo.m
#import "Foo.h"
@implementation Foo
@synthesize name;
@synthesize age;
-(id) copyWithZone: (NSZone *) zone {
Foo * aFoo = [[Foo allocWithZone:zone] init];
aFoo.name = [NSString stringWithString: self.name];
aFoo.age = [NSNumber numberWithInt:[self.age intValue]];
return aFoo;
}
-(void) encodeWithCoder: (NSCoder *) coder {
[coder encodeObject: name forKey: @"name"];
[coder encodeObject:age forKey: @"age"];
}
-(id) initWithCoder: (NSCoder *) coder {
self = [super init];
name = [[coder decodeObjectForKey:@"name"] retain];
age = [[coder decodeObjectForKey:@"age"] retain];
return self;
}
-(void) dealloc {
[name release];
[age release];
[super dealloc];
}
|
Listing 3. EncodingAppDelegate.h
#import <UIKit/UIKit.h>
@class Foo;
@class EncodingViewController;
@interface EncodingAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
EncodingViewController *viewController;
Foo * myFoo;
}
@property (nonatomic, retain) Foo * myFoo;
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet EncodingViewController
*viewController;
@end
|
Listing 4. EncodingAppDelegate.m
#import "EncodingAppDelegate.h"
#import "EncodingViewController.h"
#import "Foo.h"
@implementation EncodingAppDelegate
@synthesize window;
@synthesize viewController;
@synthesize myFoo;
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSString *pathToFile = [[NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0]
stringByAppendingPathComponent:@"foo.archive"];
NSLog(@"%@",pathToFile);
NSData * theData =[NSData dataWithContentsOfFile:pathToFile];
if([theData length] > 0) {
NSKeyedUnarchiver * archiver = [[[NSKeyedUnarchiver alloc]
initForReadingWithData:theData] autorelease];
myFoo = [archiver decodeObjectForKey:@"myfoo"];
[archiver finishDecoding];
NSLog(@"nth run - name: %@ age: %i", myFoo.name,
[myFoo.age intValue]);
}
else {
NSLog(@"first run: no name or age");
myFoo =[[Foo alloc] init];
myFoo.name = @"James";
myFoo.age = [NSNumber numberWithInt:40];
}
[window addSubview:viewController.view];
[window makeKeyAndVisible];
return YES;
}
-(void) applicationWillTerminate: (UIApplication *) application {
NSString *pathToFile = [[NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0]
stringByAppendingPathComponent:@"foo.archive"];
NSMutableData * theData = [NSMutableData data];
NSKeyedArchiver * archiver = [[[NSKeyedArchiver alloc]
initForWritingWithMutableData:theData] autorelease];
[archiver encodeObject:self.myFoo forKey:@"myfoo"];
[archiver finishEncoding];
if([theData writeToFile:pathToFile atomically:YES] == NO)
NSLog(@"writing failed.... ");
}
-(void)dealloc {
[myFoo release];
[viewController release];
[foo release];
[window release];
[super dealloc];
}
@end
|
Open the previous application, Encoding, in Xcode. Create a new Objective-C class and name it Bar. Have it adopt the NSCoding and NSCopying protocols (Listings 5 and 6). Add an NSMutableArray as a property in Bar. Override init to add a couple of Foo objects to the array (Listing 7). Implement the initWithCoder:, encodeWithCoder:, and copyWithZone: methods (Listing 8). Add
Bar as a property to EncodingAppDelegate. Remember, you must have a
forward reference to the class, since you are adding it as a property to
the header, and then import the Bar class and synthesize the property
in the implementation. Modify
EncodingAppDelegate’s applicationDidFinishLaunchingWithOptions: and
applicationWillTerminate: methods to include the newly created Bar
property. Note that we changed the name of the archive file to foo2.archive to avoid conflicting with the previous task.
|
Listing 5. Bar.h
#import <Foundation/Foundation.h>
#import "Foo.h"
@interface Bar : NSObject <NSCoding, NSCopying> {
NSMutableArray * foos;
}
@property (nonatomic, retain) NSMutableArray * foos;
@end
|
Listing 6. Bar.m
#import "Bar.h"
@implementation Bar
@synthesize foos;
-(id) init {
if([super init] == nil)
return nil;
Foo * foo1 = [[Foo alloc] init];
foo1.name = @"Juliana";
foo1.age = [NSNumber numberWithInt:7];
Foo * foo2 = [[Foo alloc] init];
foo2.name = @"Nicolas";
foo2.age = [NSNumber numberWithInt:3];
foos = [[NSMutableArray alloc] initWithObjects:foo1, foo2, nil];
return self;
}
-(void) encodeWithCoder: (NSCoder *) coder {
[coder encodeObject: foos forKey:@"foos"];
}
-(id) initWithCoder: (NSCoder *) coder {
self = [super init];
foos = [[coder decodeObjectForKey:@"foos"] retain];
return self;
}
-(id) copyWithZone: (NSZone *) zone {
Bar * aBar = [[Bar allocWithZone:zone] init];
NSMutableArray *newArray = [[[NSMutableArray alloc] initWithArray:
self.foos copyItems:YES] autorelease];
aBar.foos = newArray;
return aBar;
}
- (void) dealloc {
[foos release];
[super dealloc];
}
@end
|
Listing 7. EncodingAppDelegate.h
#import <UIKit/UIKit.h>
@class Bar
@class Foo;
@class EncodingViewController;
@interface EncodingAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
EncodingViewController *viewController;
Foo * myFoo;
Bar * myBar;
}
@property (nonatomic, retain) Foo * myFoo;
@property (nonatomic, retain) Bar * myBar;
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet EncodingViewController
*viewController;
@end
|
Listing 8. EncodingAppDelegate.m
#import "EncodingAppDelegate.h"
#import "EncodingViewController.h"
#import "Foo.h"
#import "Bar.h"
@implementation EncodingAppDelegate
@synthesize window;
@synthesize viewController;
@synthesize myFoo;
@synthesize myBar;
-(void)applicationDidFinishLaunching:(UIApplication *)application {
NSString *pathToFile = [[NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0]
stringByAppendingPathComponent:@"foo2.archive"];
NSLog(@"%@", pathToFile);
NSData * theData =[NSData dataWithContentsOfFile:pathToFile];
if([theData length] > 0) {
NSKeyedUnarchiver * archiver = [[[NSKeyedUnarchiver alloc]
initForReadingWithData:theData] autorelease];
myFoo = [archiver decodeObjectForKey:@"myfoo"];
myBar = [archiver decodeObjectForKey:@"mybar"];
[archiver finishDecoding];
NSLog(@"nth run - name: %@ age: %i", myFoo.name,
[myFoo.age intValue]);
NSArray * array = myBar.foos;
for(Foo * aFoo in array) {
NSLog(@"Foo: name: %@, age: %i", aFoo.name, [aFoo.age intValue]);
}
}
else {
NSLog(@"first run: no name or age");
myFoo =[[Foo alloc] init];
myFoo.name = @"James";
myFoo.age = [NSNumber numberWithInt:40];
myBar = [[Bar alloc] init];
}
[window addSubview:viewController.view];
[window makeKeyAndVisible];
}
-(void) applicationWillTerminate: (UIApplication *) application {
NSString *pathToFile = [[NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0]
stringByAppendingPathComponent:@"foo2.archive"];
NSMutableData * theData = [NSMutableData data];
NSKeyedArchiver * archiver = [[[NSKeyedArchiver alloc]
initForWritingWithMutableData:theData] autorelease];
[archiver encodeObject:myFoo forKey:@"myfoo"];
[archiver encodeObject:myBar forKey:@"mybar"];
[archiver finishEncoding];
if( [theData writeToFile:pathToFile atomically:YES] == NO)
NSLog(@"writing failed....");
}
-(void)dealloc {
[myFoo release];
[myBar release];
[viewController release];
[window release];
[super dealloc];
}
|
When
the application starts, it loads the archive file into a data object. If
the data object is null, the file doesn’t exist. If the file does
exist, the data is unarchived. When the application terminates, it
archives Foo. Because Bar contains constituent Foo objects in an array,
it also archives those objects. The key for the archived Foo is “myfoo,”
and “mybar” for the archived Bar object. Both Foo and Bar implement the
NSCoding protocol. This allows them to be archived. Notice that Bar
contains an NSMutableArray of Foo objects. Because NSMutableArray adopts
the NSCoding protocol, NSMutableArray can be encoded and decoded.
Moreover, the NSMutableArray knows to encode or decode its constituent
elements.
Now examine Bar’s copyWithZone method. Because Bar
contains an NSMutableArray of Foo objects, when copying a Bar you must
also copy the Bar’s Foo array. But you cannot just set the new Bar’s
array to the old Bar’s array, as the new Bar’s array will simply be a
pointer to the old Bar’s array. Instead you must create a new
NSMutableArray and initialize the new array with the old array, being
certain to specify copyItems as YES. By taking this step, the new Bar
obtains a deep copy of the old Bar’s array of Foo objects.
Note
For more information on archiving, refer to “Apple’s Archives and Serializations Programming Guide for Cocoa.”
3. Multitasking and Saving Application State
In
versions of the iOS prior to 4, when the user pressed the Home button,
your application was terminated. Now the default behavior is to leave
your application in memory and just suspend it. Then if the user wants
to return to it later, it can launch instantly and they’re exactly where
they left off. We actually had to turn this behavior off in the
encoding task so that it would terminate and save our test data. For
simple apps that launch quickly and aren’t likely to be returned to over
and over, it makes sense to set that flag and not worry about coding
for application suspension. However, for more complex applications your
users will really appreciate being able to switch to another app and
then instantly return to yours, continuing right where they left off.
So, what does this have to do with archiving? If all
the iOS ever did was temporarily freeze your app in memory and then
continue it later, there wouldn’t be any need for persistence. But, if
iOS runs low on memory or the user doesn’t return to your application
for a while, then it can be purged from memory without warning. This
creates unpredictable behavior for your users. Sometimes when they
switch to another app and then return to yours, everything is exactly as
they left it (e.g., they’re in the middle of a gaming level or tunneled
deeply down into some nested UITableViews). Other times, when they
return to your app it seems to be starting up fresh (because it is—iOS
had to purge it from memory). The recommended way to deal with this
inconsistency is to always save away enough state information when your
application is suspended so that you can restore it to the exact same
context if your application is purged from memory before the user can
return to it. That’s where archiving comes in.
The state information you’ll need to save is going to
be different for every application, but the easiest way to save that
information is going to be a set of objects serialized and archived to a
file in the application’s Documents directory. For a game you might
need to serialize a number indicating the level they were on along with a
hierarchy of objects that encode the state of enemies, puzzles, etc.,
on that level. For a reference application that browses hierarchical
data, it might be saving a trail of which item they chose at each level
and where they were scrolled to on the current view. In either case, you
will likely want to implement the NSCoding protocol for a selection of
objects, so that you can archive them when your application is
suspended. Implement the applicationDidEnterBackground method in your
AppDelegate class and save your state information there as well as
freeing up any resources you don’t need.
- (void)applicationDidEnterBackground:(UIApplication *)application
Implement the applicationWillEnterForeground method in your AppDelegate class to restore your state information.
- (void)applicationWillEnterForeground:(UIApplication *)application