Chapter 15
Property Lists and Archiving

Key Skills & Concepts

• Understanding the iOS directory structure

• Persisting a collection as a property list

• Archiving an object hierarchy

In this chapter, you learn how to persist your data to a file using properties and then how to persist your data using archiving. However, before learning about these two topics, you briefly explore the iOS’s file system.

An iOS Application’s Directory Structure

Persisting and archiving require writing data to a file, but an iOS application can only read and write to files in the application’s sandbox. When installed, an application is placed in its own home directory. This directory is the application’s root directory and should be left untouched, lest you risk corrupting your application. Under the application’s home directory are the directories you may write to. These directories are the Documents, Preferences, Caches, and tmp directories.

<application home directory>/Documents
<application home directory>/Library/Preferences
<application home directory>/Library/Caches
<application home directory>/tmp

The Documents directory is where you should write your application’s data files. The Preferences directory is where your application’s preferences are stored. These are the preferences set through the iOS’s Settings application, using the NSUserDefaults class, not preferences you might create programmatically. The Caches directory, like the Documents directory, is another location you can persist files to, although, as the name implies, this directory should be reserved for caching data rather than storing an application’s files. The tmp directory is a temporary directory for writing files that do not need to be persisted between application launches. Your application should remove files from this directory when not needed and iOS also removes files from this folder when an application is not running.

Directories

You will mostly read and write from two directories: the Documents directory and the tmp directory. Files you want to persist between application launches should go in the Documents directory. These files are also backed up by iTunes when an iPhone, iPod touch, or iPad is synchronized with a user’s computer. Files placed in the tmp folder are temporary and should be deleted when an application terminates. If the application does not clean the folder, iOS might delete them depending on when space is needed on your device. You should never hard-code a path in your code to either folder. When using the Documents folder, you should use the NSHomeDirectory method combined with the NSSearchPathForDirectoriesInDomain method. When obtaining the tmp directory, you should use the NSTemporaryDirectory method.

NSHomeDirectory

The NSHomeDirectory is how you should obtain an application’s root directory.

NSString * NSHomeDirectory (void);

Obtain the path to your Documents directory using the NSHomeDirectory. By itself, this method isn’t very useful, as you usually want to obtain the Documents directory.

NSSearchPathForDirectoriesInDomains

Obtain the path to your application’s Documents directory using the NSSearchPathFor DirectoriesInDomains.

NSArray * NSSearchPathForDirectoriesInDomains (
    NSSearchPathDirectory directory,
NSSearchPathDomainMask domainMask, BOOL expandTilde );

The method takes three parameters: the directory to begin the search, the search path domain mask, and a flag indicating if tildes should be converted to actual paths. The method returns an array of paths. Although on a desktop or laptop there might be multiple elements in the array, on an iOS device, there will only be one result in the array. The following code illustrates how to obtain an application’s Documents directory on an iOS device:

NSArray * myPaths = NSSearchPathForDirectoriesInDomains(
    NSDocumentDirectory, NSUserDomainMask,
YES); NSString * myDocPath = [myPaths objectAtIndex:0];

Values you might use for the directory parameter on an iOS device include NSDocumentDirectory, NSApplicationDirectory, NSCachesDirectory, and NSApplicationSupportDirectory.

NSTemporaryDirectory

The NSTemporaryDirectory method returns the path to your application’s tmp directory.

NSString * NSTemporaryDirectory (void);

Unlike the NSHomeDirectory method, the NSTemporaryDirectory method is useful by itself, as it is the most direct way to obtain a path to your application’s tmp directory.

Property Lists

The easiest way to save your application’s preferences if you’re managing them within your application is using a property list. If an object can be serialized, you can persist it to a file using a path or URL. You can also reconstitute the object by reading it from the file.

It is worth noting that only objects can be serialized. A common source of frustration is trying to serialize a primitive int. Since primitive data types are not serializable, they need to be converted to NSObjects (e.g., int to NSNumber).

Simple Serialization

The NSDictionary, NSArray, NSString, NSNumber, and NSData classes, and their mutable equivalents, can all be saved as a property list using the writeToFile: or writeToURL: method.

-(BOOL)writeToFile:(NSString *) path atomically:(BOOL)flag
-(BOOL)writeToURL:(NSURL *) aURL atomically:(BOOL)flag

The first parameter is the path, or URL, to save the file as. The second parameter is a flag indicating if the file should first be saved to an auxiliary file. If the flag is YES, the data is written to an auxiliary file that is then renamed to the file indicated by the path or URL. Writing to an auxiliary file prevents the file system from becoming corrupt should writing the file fail midstream.

NOTE
You can refer to the NSDictionary, NSArray, NSString, NSNumber, or NSData classes, or one of their mutable equivalents, as a property list object. So you could say “the property list objects all contain . . .” rather than naming each property list object individually.

Reading a property list back into the object uses the initWithContentsOfFile: or initWithContentsOfURL: method.

-(id)initWithContentsOfFile:(NSString *)path
-(id)initWithContentsOfURL:(NSURL *)aURL

The initWithContentsOfFile: method takes a path to the property file, while the initWithContentsOfURL: takes a URL. Both return an id.

Try This
Preserving an NSArray

1. Create a new View-based Application named SimpleArray.

2. Open SimpleArrayAppDelegate.m and modify applicationDidFinishLaunching WithOptions to match Listing 15-1.

3. Click Run (Listing 15-2).

4. After running the application, navigate to properties.plist in the file system and open it using TextEdit (Listing 15-3).

Listing 15-1 The applicationDidFinishLaunching method in SimpleArrayAppDelegate.m

(BOOL)application:(UIApplication *)application
          didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  NSMutableArray * dataArray = [[NSMutableArray alloc]
         initWithObjects: @"First", @"Second", @"Third", nil];
  NSString * path = [(NSString *) [NSSearchPathForDirectoriesInDomains
       (NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]
       stringByAppendingPathComponent:@"properties.plist"];
  [dataArray writeToFile:path atomically:YES];
  NSArray * dataArray2 = [[NSArray alloc] initWithContentsOfFile:path];
  NSLog(@"objects: %@, %@, %@",
        [dataArray2 objectAtIndex:0], [dataArray2 objectAtIndex:1],
        [dataArray2 objectAtIndex:2]);
  [window addSubview:viewController.view];
  [window makeKeyAndVisible];
  [dataArray release];
  [dataArray2 release];
}

Listing 15-2 Logging to the Debugger Console

2010-09-11 11:17:41.591 SimpleArray[14500:207] objects: First, Second,
Third

Listing 15-3 The properties.plist file is saved as XML.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>First</string>
<string>Second</string>
<string>Third</string>
</array>
</plist>

The application first gets a path to its Documents directory and adds the filename to the path. After creating the path, the application persists the array to a file. Immediately after persisting the file, it creates a new array from the file’s content and logs the array’s values to the Debugger Console.

One thing interesting to note is that the application persists the array in an XML format. If you wished, you could easily modify the data in any text editor. You could also persist the application to a URL, and since it is XML with a published document type definition (DTD), you could process the file with almost any back-end programming language that had libraries for parsing XML and DTD files. However, note that the writeToURL:atomically: method is synchronous and your application will halt processing until the data is successfully written to the URL, so you are better off using the NSURLConnection class so that your application doesn’t appear to freeze up until all of the data has been written.

NSPropertyListSerialization

Using the writeToFile: method to save a property list object as a simple property list is usually sufficient, but another way you can persist a property list object to a property list is by using the NSPropertyListSerialization class.

Serializing

To serialize a property list object, use the dataFromPropertyList:format:errorDescription: method.

+(NSData *)dataFromPropertyList:(id)plist format:
(NSPropertyListFormat *)format
errorDescription:(NSString **) errorString

This method’s first parameter is an id that references the property list data and must be a property list object. Note that the dataFromPropertyList:format:errorDescription: method doesn’t open and read a file’s content; you must first obtain the data using the initWithContentsOfFile: or initWithContentsOfURL: method. The method’s second parameter is the property list’s desired format. This parameter is one of the valid NSPropertyListFormat types: NSPropertyListOpenStepFormat, NSPropertyListXMLFormat_ v1_0, or NSPropertyListBinaryFormat_v1_0. The method’s final parameter is a string to place an error description should something fail. Note, you must release this string should an error occur. The method returns an NSData object. You can then write this object to disk, using the writeToFile: or writeToURL: method.

Deserializing

To deserialize a property list, use the propertyListFromData:mutabilityOption:format: errorDescription: method.

+ (id)propertyListFromData:(NSData *)data
     mutabilityOption: (NSPropertyListMutabilityOptions) opt
     format: (NSPropertyListFormat *)format
     errorDescription:(NSString **) errorString

This method’s first parameter is the data to deserialize. The method’s second parameter indicates if the properties should be immutable or mutable. The method’s third parameter indicates the format to make the property list, and the fourth parameter is the error description. Valid values for the second parameter are NSPropertyListImmutable, NSPropertyListMutableContainers, and NSPropertyListMutableContainersAndLeaves. Valid values for the third parameter are NSPropertyListOpenStepFormat, NSPropertyListXMLFormat_v1_0, and NSPropertyListBinary Format_v1_0. Note that as with the dataFromPropertyList: method, should something fail, you must release the NSString holding the error description.

NOTE
Do not take this task’s more complex data structure as implying you cannot use a property list object’s writeToFile: or writeToURL: method to persist complex data structures. You can, provided all items in a data structure are a property list object. For instance, if an NSArray’s elements each contained an NSDictionary, you could serialize the entire data structure at once by writing the NSArray to a file.

Try This
Preserving to an XML Property List

1. Create a new View-based Application named Properties.

2. Open PropertiesAppDelegate.m and modify the applicationDidFinishLaunching WithOptions method (Listing 15-4).

3. Click Build And Go.

Listing 15-4 The PropertiesAppDelegate’s applicationDidFinishLaunchingWithOptions method

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  NSString * errorDescription;
  NSString *pathToFile = [[NSSearchPathForDirectoriesInDomains
        (NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0]
        stringByAppendingPathComponent:@"properties.plist"];
  NSData * myData;
  NSLog(@"%@", pathToFile);
  if ([[NSFileManager defaultManager] fileExistsAtPath:pathToFile] == NO) {
    NSMutableDictionary * dict2Serialize =
             [[[NSMutableDictionary alloc] init] autorelease];
    NSString * name = @"James";
    NSArray * kids = [NSArray arrayWithObjects:
                        @"Nicolas", @"Juliana", nil];
     NSNumber * age = [NSNumber numberWithInt:40];
     [dict2Serialize setObject:name forKey:@"name"];
     [dict2Serialize setObject:kids forKey:@"kids"];
     [dict2Serialize setObject:age forKey:@"age"];
     myData = [NSPropertyListSerialization dataFromPropertyList:(id)
        dict2Serialize format:NSPropertyListXMLFormat_v1_0
         errorDescription:&errorDescription];
    if (myData)
        [myData writeToFile:pathToFile atomically:YES];
    else {
         NSLog(@"Error writing to myData, error: %@", errorDescription);
         [errorDescription release];
     }
  }
  else {
    NSLog(@"property file exists....");
    NSPropertyListFormat format;
    NSData * plistData = [NSData dataWithContentsOfFile:pathToFile];
     NSDictionary * props = (NSDictionary *)[NSPropertyListSerialization
               propertyListFromData:plistData
              mutabilityOption:NSPropertyListImmutable
               format: &format errorDescription: &errorDescription];
    if (props) {
        NSLog(@"name: %@", [props objectForKey:@"name"]);
        NSLog(@"age: %i",
           [(NSNumber *)[props objectForKey:@"age"] intValue]);
         NSLog(@"kid: %@", (NSString *)[(NSArray *)
                     [props objectForKey:@"kids"] objectAtIndex:0]);
         NSLog(@"kid: %@", (NSString *)[(NSArray *)
                     [props objectForKey:@"kids"] objectAtIndex:1]);
    } else {
        NSLog(@"Error reading properties, error: %@", errorDescription);
         [errorDescription release];
     }
  }
  [window addSubview:viewController.view];
  [window makeKeyAndVisible];
  return YES;
}

The first time you run the application, the debugger output will contain only a path. The second time, however, the application logs the property list contents to the Debugger Console. Notice that rather than writing the NSDictionary directly to disk, you first transformed it into an NSData object representing the property list. Had this first step of converting to XML gone awry, you would have the error description informing you (hopefully) where the problem occurred. This error handling is not provided using the NSMutableDictionary’s writeToFile: method. After converting to a property list, you then persisted it using the NSData’s writeToFile method. Listing 15-5 lists the file’s XML content. Upon running the application a second time, you read the property list as an NSData object and converted it to an NSDictionary. To prove that the data was in fact reconstituted correctly, you logged the output to the Debugger Console (Listing 15-6).

Listing 15-5 The application’s plist saved as XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www
.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>age</key>
<integer>40</integer>
<key>kids</key>
<array>
<string>Nicolas</string>
<string>Juliana</string>
</array>
<key>name</key>
<string>James</string>
</dict> </plist>

Listing 15-6 The application’s Debugger Console logging

2010-09-11 11:35:07.918 Properties[14672:207] /Users/bward/Library/
Application Support/iPhone Simulator/4.1/Applications/3D6D7BC3-8957-
4F2A-977B-6016E86F28C4/Documents/properties.plist
2010-09-11 11:35:07.920 Properties[14672:207] property file exists....
2010-09-11 11:35:07.922 Properties[14672:207] name: James
2010-09-11 11:35:07.922 Properties[14672:207] age: 40
2010-09-11 11:35:07.924 Properties[14672:207] kid: Nicolas
2010-09-11 11:35:07.925 Properties[14672:207] kid: Juliana

Archiving

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.

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.

NOTE
This chapter only discusses NSCopying briefly, as it is not used in this chapter. It is included because best practices dictate that if a class implements the NSCoding protocol for archiving, it must also implement the NSCopying protocol.

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];

Try This
Archiving and Unarchiving an Object

1. Create a new View-based Application called Encoding.

2. Create a new Objective-C class called Foo.

3. 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.”

4. Have Foo adopt the NSCopying and NSCoding protocols (Listings 15-7 and 15-8). Remember, Foo must get deep copies of name and age.

5. Modify Foo so that it implements the encodeWithCoder:, initWithCoder:, and copyWithZone: methods.

6. Add Foo as a property to EncodingAppDelegate (Listings 15-9 and 15-10).

7. Implement the applicationWillTerminate: method and modify the applicationDidFinish LaunchingWithOptions: method to decode and encode Foo.

8. 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.)

9. 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 15-7 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 15-8 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 15-9 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 15-10 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

Try This
Archiving and Unarchiving an Object Hierarchy

1. Open the previous application, Encoding, in Xcode.

2. Create a new Objective-C class and name it Bar. Have it adopt the NSCoding and NSCopying protocols (Listings 15-11 and 15-12).

3. Add an NSMutableArray as a property in Bar.

4. Override init to add a couple of Foo objects to the array (Listing 15-13).

5. Implement the initWithCoder:, encodeWithCoder:, and copyWithZone: methods (Listing 15-14).

6. 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.

7. Modify EncodingAppDelegate’s applicationDidFinishLaunchingWithOptions: and applicationWillTerminate: methods to include the newly created Bar property.

8. Note that we changed the name of the archive file to foo2.archive to avoid conflicting with the previous task.

9. Click Run.

Listing 15-11 Bar.h

#import <Foundation/Foundation.h>
#import "Foo.h"
@interface Bar : NSObject <NSCoding, NSCopying> {
 NSMutableArray * foos;
}
@property (nonatomic, retain) NSMutableArray * foos;
@end

Listing 15-12 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 15-13 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 15-14 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.”

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

Summary

In this chapter, you learned how to persist an application’s data using property lists and archiving. These techniques are really only practical for persisting a few reasonably small objects to a file. Large object hierarchies are much better persisted using SQLite or better yet, the Core Data framework. But for state information, user preferences, etc., persisting to a property list or archiving is fine. In this chapter, you learned methods for doing both. As a rule of thumb, if persisting variables not tied to particular objects, simply place them in a collection and persist the collection as a property list. But if persisting variables that are object properties, have the objects adopt the NSCoding protocol and archive the objects. If persisting a moderate to large amount of data, use SQLite or use the Core Data framework. If you want to use the data outside of an iOS or Cocoa application, you should use SQLite. Both SQLite and Core Data will be discussed in upcoming chapters.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset