19. Archiving

In Objective-C terms, archiving is the process of saving one or more objects in a format so that they can later be restored. Often, this involves writing the object(s) to a file so it can subsequently be read back in. We will discuss two methods for archiving data in this chapter: property lists and coding.

Archiving with Property Lists

Applications on Mac OS X use lists (or plists) for storing things such as your default preferences, application settings, and configuration information, so it's useful to know how to create them and read them back in. Their use for archiving purposes, however, is limited because when creating a property list for a data structure, specific object classes are not retained, multiple references to the same object are not stored, and the mutability of an object is not preserved.

If your objects are NSString, NSDictionary, NSArray, or NSData objects, you can use the writeToFile: method implemented in these classes to write your data to a file. In the case of writing out a dictionary or an array, this method writes the data out in the format of a property list. On the Mac, the property list created is in a format known as XML by default, which is an HTML-like language.1 On GNUStep systems, a different format for property lists, known as the traditional, old-style, or ASCII (depending upon whether you're speaking to GNU or Mac programmers) format is used by default.

Program 19.1 shows how the dictionary you created as a simple glossary in Chapter 15, “Numbers, Strings, and Collections,” can be written to a file as a property list.

Program 19.1.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];
  NSDictionary *glossary =
   [NSDictionary
      dictionaryWithObjectsAndKeys:
        @"A class defined so other classes can inherit from it.",
        @"abstract class",
        @"To implement all the methods defined in a protocol",
        @"adopt",
        @"Storing an object for later use. ",
        @"archiving",
        nil
        ];

  if ([glossary writeToFile: @"glossary" atomically: YES] == NO)
    printf ("Save to file failed! ");

  [pool release];
  return 0;
}


The writeToFile:atomically: message is sent to your dictionary object glossary, causing the dictionary to be written to the file glossary in the form of a property list. The atomically parameter is set to YES, meaning you want the write operation to be done to a temporary backup file first and once successful, the final data is to be moved to the specified file named glossary. This is a safeguard that protects the file from becoming corrupt if, for example, the system crashes in the middle of the write operation. In that case, the original glossary file (if it previously existed) isn't harmed.

If you look at the glossary file created by Program 19.1, it looks like this on the Mac:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
            "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>abstract class</key>
    <string>A class defined so other classes can inherit from it.</string>
    <key>adopt</key>
    <string>To implement all the methods defined in a protocol</string>
    <key>archiving</key>
    <string>Storing an object for later use. </string>
</dict>
</plist>

As noted, under GNUStep, the property list is written in a traditional format by default. The glossary file looks like this:

{
  "abstract class" = "A class defined so other classes can inherit from it.";
  adopt = "To implement all the methods defined in a protocol";
  archiving = "Storing an object for later use. ";
}

GNUStep programs can generate XML-style property lists by changing the user default settings GSMacOSXCompatible or NSWriteOldStylePropertyLists. Look at the NSUserDefaults class and check your GNUStep documentation for more details. Mac users can create traditional or old-style property lists using the dataFromPropertyList:format:errorDescription: method from the NSPropertyListSerialization class and specifying NSPropertyListOpenStepFormat as the format parameter. The Mac and GNUStep systems can read property lists created in either format.

If you create a property list from a dictionary, the keys in the dictionary must all be NSString objects. The elements of an array or the values in a dictionary can be NSString, NSArray, NSDictionary, NSData, NSDate, or NSNumber objects.

To read a property list from a file into your program, you use the dictionaryWithContentsOfFile: or arrayWithContentsOfFile: method. To read back data, use the dataWithContentsOfFile: method, and to read back string objects, use the stringWithContentsOfFile: method. Program 19.2 reads back the glossary written in Program 19.1 and then prints it contents.

Program 19.2.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];
  NSDictionary *glossary;
  NSEnumerator *keyEnum;
  id     key;

  glossary = [NSDictionary dictionaryWithContentsOfFile: @"glossary"];

  keyEnum = [glossary keyEnumerator];

  while ( (key = [keyEnum nextObject]) != nil ) {
        printf ("%s: %s ", [key cString],
        [[glossary objectForKey: key] cString]);
  }

  [pool release];
  return 0;
}


Program 19.2. Output


archiving: Storing an object for later use.

abstract class: A class defined so other classes can inherit from it.

adopt: To implement all the methods defined in a protocol


Your property lists don't need to be created from an Objective-C program; the property list can come from any source. You can make your own property lists using a simple text editor or use the Property List Editor program located in the /Developer/Applications directory on Mac OS X systems.

If you have a property list stored in an NSString object, you can convert it into its equivalent dictionary, string, array, or data object by sending it a propertyList message. For example, consider the following code sequence:

NSString *months = @"{
  "january" = 31; "february" = 28; "march" = 31;
  "april" = 30; "may" = 31; "june" = 30; "july" = 31;
  "august" = 31; "september" = 30; "october" = 31;
  "november" = 30; "december" = 31; }";

daysOfMonthDict = [months propertyList];

(Recall that to continue a long character string over several lines, a backslash character must appear as the very last character on the line. Also, to include a double quotation mark in a string, it must be escaped by preceding it with a backslash character.)

This sequence takes the dictionary encoded as a traditional property list in the string object months and converts it to a dictionary object. To subsequently get the number of days in July, for example, you could then write the following:

[[daysOfMonthDict objectForKey: @"july"] intValue]

Obviously, this is a contrived example because there would be no advantage here of hard-coding a property list into your program. The dictionary could have been set up directly.

Property Lists and URLs

Incidentally, you can also write a property list to a URL and read one from a URL. For example, the statement

glossary = [NSDictionary dictionaryWithContentsOfURL:
     [NSURL URLWithString:
          @"http://www.kochan-wood.com/examples/glossary.pl"]];

creates a dictionary from the property list stored at the specified URL.

The dictionaryWithContentsOfURL: method is not currently implemented under GNUStep. Another way to read the property list from a Web site is to use the stringWithContentsOfURL: method to first read it into a string and then convert it to a property list using the propertyList method. The can be done with a statement like this:

glossary = [[NSString stringWithContentsOfURL:
   [NSURL URLWithString: @"http://www.kochan-wood.com/examples/glossary.pl"]
    ] propertyList];

Data can be written to a URL using the writeToURL:atomically: method. For more information about working with URLs, consult the documentation for the Foundation class NSURL and the header file <Foundation/NSURL.h>.

Archiving with NSArchiver

A more flexible approach allows for the saving of any types of objects to a file, not just strings, arrays, and dictionaries. Let's begin with the simple glossary created in Chapter 15. Program 19.3 shows how the glossary can be saved to a file on disk using the method archiveRootObject:toFile: from the NSArchiver class. To use this class, include the file

#import <Foundation/NSArchiver.h>

in your program.

Program 19.3.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSAutoreleasePool.h>


int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSDictionary *glossary =
   [NSDictionary dictionaryWithObjectsAndKeys:
     @"A class defined so other classes can inherit from it",
     @"abstract class",
     @"To implement all the methods defined in a protocol",
     @"adopt",
     @"Storing an object for later use",
     @"archiving",
     nil
   ];


  [NSArchiver archiveRootObject: glossary toFile: @"glossary.archive"];

  [pool release];
  return 0;
}


Program 19.3 does not produce any output at the terminal. However, the statement

[NSArchiver archiveRootObject: glossary toFile: @"glossary.archive"];

writes the dictionary glossary to the file glossary.archive. Any pathname can be specified for the file. In this case, the file is written to the current directory.

The archive file created can later be read into an executing program by using NSUnarchiver's unArchiveObjectWithFile: method, as is done in Program 19.4.

Program 19.4.


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool  *pool = [[NSAutoreleasePool alloc] init];
  NSDictionary *glossary;
  NSEnumerator *keyEnum;
  id       key;

  glossary = [NSUnarchiver unarchiveObjectWithFile:
                  @"glossary.archive"];

  keyEnum = [glossary keyEnumerator];

  while ( (key = [keyEnum nextObject]) != nil ) {
        printf ("%s: %s ", [key cString],
        [[glossary objectForKey: key] cString]);
  }

  [pool release];
  return 0;
}


Program 19.4. Output


abstract class: A class defined so other classes can inherit from it.

adopt: To implement all the methods defined in a protocol

archiving: Storing an object for later use.


The statement

glossary = [NSUnarchiver unarchiveObjectWithFile:
                @"glossary.archive"];

causes the specified file to be opened and its contents to be read. This file must be the result of a previous archive operation. You can specify a full pathname for the file or a relative path name, as was done in the example.

After the glossary has been restored, the program simply enumerates its contents to verify that the restore was successful.

Writing Encoding and Decoding Methods

Basic Objective-C class types such as NSString, NSArray, NSDictionary, NSSet, NSDate, NSNumber, and NSData can be archived and restored in the manner just described. That includes nested objects as well, such as an array containing string or even other array objects.

This implies that you can't directly archive your AddressBook using this method because the Objective-C system doesn't know how to archive an AddressBook object. If you were to try to archive it by inserting a line such as

[NSArchiver archiveRootObject: myBook toFile: @"addrbook.arch"];

into your program, you'd get the following message displayed if you ran the program under Mac OS X:

2003-07-23 12:03:05.267 a.out[3516] *** -[AddressBook encodeWithCoder:]:
  selector not recognized
2003-07-23 12:03:05.268 a.out[3516] *** Uncaught exception:
<NSInvalidArgumentException>  *** -[AddressBook encodeWithCoder:]:
selector not recognized
a.out: received signal: Trace/BPT trap

From the error messages, you can see that the system was looking for a method called encodeWithCoder: in the AddressBook class, but you never defined such a method.

To archive objects other than those listed, you have to tell the system how to archive, or encode, your objects and also how to unarchive, or decode, them. This is done by adding encodeWithCoder: and initWithCoder: methods to your class definitions according to the <NSCoding> protocol. For our address book example, you'd have to add these methods to both the AddressBook and AddressCard classes.

The encodeWithCoder: method is invoked each time the archiver wants to encode an object from the specified class, and the method tells it how to do so. In a similar manner, the initWithCoder: method is invoked each time an object from the specified class is to be decoded.

In general, the encoder method should specify how to archive each instance variable in the object you want to save. Luckily, you have help doing this. For the basic Objective-C classes described previously, you can use the encodeObject: method. On the other hand, for basic Objective-C data types (such as integers and floats), you must use a slightly more involved method called encodeValueOfObjCType:at:. The decoder method, initWithCoder:, works in reverse: You use decodeObject for decoding basic Objective-C classes and decodeValueOfObjCType:at: for the basic data types.

Program 19.5 adds the two encoding and decoding methods to both the AddressCard and AddressBook classes.

Program 19.5. Addresscard.h Interface File


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArchiver.h>

@interface AddressCard: NSObject <NSCoding, NSCopying>
{
  NSString  *name;
  NSString  *email;
}

-(void) setName: (NSString *) theName;
-(void) setEmail: (NSString *) theEmail;
-(void) setName: (NSString *) theName andEmail: (NSString *) theEmail;


-(NSString *) name;
-(NSString *) email;

-(NSComparisonResult) compareNames: (id) element;

-(void) print;

// Additional methods for NSCopying protocol
-(AddressCard *) copyWithZone: (NSZone *) zone;
-(void) retainName: (NSString *) theName andEmail: (NSString *) theEmail;

@end


Here are the two new methods for your AddressCard class to be added to the implementation file:

-(void) encodeWithCoder: (NSCoder *) encoder
{
  [encoder encodeObject: name];
  [encoder encodeObject: email];
}

-(id) initWithCoder: (NSCoder *) decoder
{
  name = [[decoder decodeObject] retain];
  email = [[decoder decodeObject] retain];

  return self;
}

The encoding method encodeWithCoder: is passed an NSCoder object as its argument. For each object you want to encode, you send a message to this object. In the case of your address book, you have two instance variables called name and email. Because these are both NSString objects, you use the encodeObject: method to encode each of them in turn. These two instance variables are then added to the archive. Note that encodeObject: can be used for any object that has implemented a corresponding encodeWithCoder: method in its class.

The decoding process works in reverse. The argument passed to initWithCoder: is again an NSCoder object. You don't need to worry about this argument; just remember that it's the one that gets the messages for each object you want to extract from the archive.

Because you've stored two objects in the archive with the encoding method, when decoding you must extract them in the same order in which they were added. First, you use the decodeObject message to get your name decoded, followed by a second message to get the email. You retain both instance variables to ensure that they still exist and are valid after the unarchiving process is completed. Note that the decoding method is expected to return itself.

Similarly to your AddressCard class, you add encoding and decoding methods to your AddressBook class. The only line you need to change in your interface file is the @interface directive to declare that the AddressBook class now conforms to the NSCoding protocol. The change looks like this:

@interface AddressBook: NSObject <NSCoding, NSCopying>

Here are the method definitions for inclusion in the implementation file:

-(void) encodeWithCoder: (NSCoder *) encoder
{

  [encoder encodeObject: bookName];
  [encoder encodeObject: book];
}

-(id) initWithCoder: (NSCoder *) decoder
{
   bookName = [[decoder decodeObject] retain];
   book = [[decoder decodeObject] retain];

   return self;
}

The test program is shown next as Program 19.6.

Program 19.6. Test Program


#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  NSString  *aName = @"Julia Kochan";
  NSString  *aEmail = @"[email protected]";
  NSString  *bName = @"Tony Iannino";
  NSString  *bEmail = @"[email protected]";
  NSString  *cName = @"Stephen Kochan";
  NSString  *cEmail = @"[email protected]";
  NSString  *dName = @"Jamie Baker";
  NSString  *dEmail = @"[email protected]";

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  AddressCard *card1 = [[AddressCard alloc] init];
  AddressCard *card2 = [[AddressCard alloc] init];
  AddressCard *card3 = [[AddressCard alloc] init];
  AddressCard *card4 = [[AddressCard alloc] init];

  AddressBook  *myBook = [AddressBook alloc];


  // First set up four address cards

  [card1 setName: aName andEmail: aEmail];
  [card2 setName: bName andEmail: bEmail];
  [card3 setName: cName andEmail: cEmail];
  [card4 setName: dName andEmail: dEmail];

  myBook = [myBook initWithName: @"Steve's Address Book"];

  // Add some cards to the address book

  [myBook addCard: card1];
  [myBook addCard: card2];
  [myBook addCard: card3];
  [myBook addCard: card4];

  [myBook sort];

  if ([NSArchiver archiveRootObject: myBook toFile: @"addrbook.arch"] == NO)
    printf ("archiving failed ");

  [card1 release];
  [card2 release];
  [card3 release];
  [card4 release];
  [myBook release];

  [pool release];
  return 0;
}


This program creates the address book and then archives it to the file addrbook.arch. In the process of creating the archive file, realize that the encoding methods from both the AddressBook and AddressCard classes were invoked. You can add some printf calls to these methods if you want proof.

Program 19.7 shows how you can read the archive into memory to set up the address book from a file.

Program 19.7.


#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>

int main (int argc, char *argv[])
{
  AddressBook         *myBook;
  NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];

  myBook = [NSUnarchiver unarchiveObjectWithFile: @"addrbook.arch"];

  [myBook list];

  [pool release];
  return 0;
}


Program 19.7. Output


======== Contents of: Steve's Address Book =========
Jamie Baker       [email protected]
Julia Kochan      [email protected]
Stephen Kochan    [email protected]
Tony Iannino      [email protected]
====================================================


In the process of unarchiving the address book, the decoding methods added to your two classes were automatically invoked. Notice how easily you can read the address book back into the program.

Archiving Basic Objective-C Data Types

The encodeObject: method works for built-in classes and classes for which you write your encoding and decoding methods according to the NSCoding protocol. If your instance contains some basic data types, such as integers or floats, you'll need to know how to encode and decode them. Here's a simple definition for a class called Foo that contains three instance variables—one is an NSString, another is an int, and the third is a float. The class has one setter method, three getters, and two encoding/decoding methods to be used for archiving:

@interface Foo: NSObject <NSCoding>
{
  NSString *strVal;
  int      intVal;
  float    floatVal;
}

-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff;
-(NSString *)   strVal;
-(int)          intVal;
-(float)        floatVal;
@end

The implementation file follows:

@implementation Foo;

-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff
{
  strVal = ss;
  intVal = ii;
  floatVal = ff;
}

-(NSString *) strVal  { return strVal; }
-(int)        intVal  { return intVal; }
-(float)      floatVal { return floatVal; }

-(void) encodeWithCoder: (NSCoder *) encoder
{
  [encoder encodeObject: strVal];
     [encoder encodeValueOfObjCType: @encode(int) at: &intVal];
     [encoder encodeValueOfObjCType: @encode(float) at: &floatVal];
}

-(id) initWithCoder: (NSCoder *) decoder
{
     strVal = [[decoder decodeObject] retain];
     [decoder decodeValueOfObjCType: @encode(int) at: &intVal];
     [decoder decodeValueOfObjCType: @encode(float) at: &floatVal];

     return self;
}
@end

The encoding routine first encodes the string value strVal using the encodeObject method you used before. Next, you need to encode your integer and float fields. The method encodeValueOfObjCType:at: takes two arguments to encode a basic Objective-C data type. The first is a special encoding obtained by applying the @encode directive to the data type name. Because intVal is an integer data type, you write @encode(int) as the argument. The second argument is a pointer (as you encountered with the fileExistsAtPath:isDir: method in Chapter 16, “Working with Files”) to the actual instance variable to be encoded and can be created by applying the address operator (&) to the variable.

The floating variable floatVal is encoded in a similar manner, by passing the arguments @encode(float) and &floatVal to the encodeValueOfObjCType:at: method.

When decoding a basic data type, you use the decodeValueOfObjCType:at: method, and the arguments are the same. In this case, the value decoded is stored at the memory address specified by the at: argument.

You don't retain basic data types. They aren't objects, so they can't be retained.

In Program 19.8, a Foo object is created, archived to a file, unarchived, and then displayed.

Program 19.8. Test Program


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSAutoreleasePool.h>
#import "Foo.h"  // Definition for our Foo class

int main (int argc, char *argv[])
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    Foo *myFoo1 = [[Foo alloc] init];
    Foo *myFoo2;

    [myFoo1 setAll: @"This is the string" iVal: 12345 fVal: 98.6];
    [NSArchiver archiveRootObject: myFoo1 toFile: @"foo.arch"];

    myFoo2 = [NSUnarchiver unarchiveObjectWithFile: @"foo.arch"];
    printf ("%s %i %g ", [[myFoo2 strVal] cString],
                          [myFoo2 intVal], [myFoo2 floatVal]);
    [myFoo1 release];
    [pool release];
    return 0;
}


Program 19.8. Output


This is the string
12345
98.6


Keyed Archives2

We noted in the discussion of the decoder methods that the fields in an archive must be read back in precisely the same order in which they were written. This technique might suit you; however, if you are creating archives from programs that might be going through many revisions, you might reorder some of the instance variables in one of your class definitions, or perhaps even add or remove some. In that case, restoring a previously created archive would be next to impossible.

A keyed archive is one in which each field of the archive has a name. When you archive an object, you give it a name, or key. When you retrieve it from the archive, you retrieve it by the same key. In that manner, objects can be written to the archive and retrieved in any order. Further, if new instance variables are added or removed to a class, the decoding method can account for it—for example, by setting a default value to a key that does not exist in the archive (for instance, if the archive were created by a different version of the program).

Instead of importing the file <Foundation/NSArchiver.h> in your interface file, to work with keyed archives you need to import <Foundation/NSKeyedArchiver.h>.

Referring to the Foo class defined in the previous example, if you define a class that others will use, you don't really know whether they'll try to archive objects from your class using keyed archiving. To account for that, you can write your encoding and decoding methods to handle either keyed or unkeyed archives. This can be done by sending an allowsKeyedCoding message to the encoder sent to your encodeWithCoder: method. If the answer is YES, you should use keyed archiving; otherwise, archive your objects in the manner described in the previous section. The same thing applies to your decoder: First, test whether keyed archiving is in effect and if it is, decode your instance variables accordingly.

Program 19.9 shows the modified Foo class interface and implementation files to allow for keyed archiving.

Program 19.9. Foo Interface File


@interface Foo: NSObject <NSCoding>
{
  NSString *strVal;
  int    intVal;
  float  floatVal;
}

-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff;

-(NSString *) strVal;
-(int) intVal;
-(float) floatVal;
@end


The interface file hasn't changed from the previous example, but the implementation file has.

Program 19.9. Foo Implementation File


@implementation Foo;
-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff
{
  strVal = ss;
  intVal = ii;
  floatVal = ff;
}

-(NSString *) strVal  { return strVal; }
-(int)        intVal  { return intVal; }
-(float)      floatVal { return floatVal; }


-(void) encodeWithCoder: (NSCoder *) encoder
{
    if ( [encoder allowsKeyedCoding] ) {
     [encoder encodeObject: strVal forKey: @"FoostrVal"];
     [encoder encodeInt: intVal forKey: @"FoointVal"];
     [encoder encodeFloat: floatVal forKey: @"FoofloatVal"];
    } else {
     [encoder encodeObject: strVal];
     [encoder encodeValueOfObjCType: @encode(int) at: &intVal];
     [encoder encodeValueOfObjCType: @encode(float) at: &floatVal];
    }
}

-(id) initWithCoder: (NSCoder *) decoder
{
    if ( [decoder allowsKeyedCoding] ) {
     strVal = [[decoder decodeObjectForKey: @"FoostrVal"] retain];
     intVal = [decoder decodeIntForKey: @"FoointVal"];
     floatVal = [decoder decodeFloatForKey: @"FoofloatVal"];
    } else {
     strVal = [[decoder decodeObject] retain];
     [decoder decodeValueOfObjCType: @encode(int) at: &intVal];
     [decoder decodeValueOfObjCType: @encode(float) at: &floatVal];
  }

  return self;
}
@end


After testing for keyed archiving, the three messages

[encoder encodeObject: strVal forKey: @"FoostrVal"];
[encoder encodeInt: intVal forKey: @"FoointVal"];
[encoder encodeFloat: floatVal forKey: @"FoofloatVal"];

archive the three instance variables from the object. The encodeObject:forKey: method encodes an object and stores it under the specified key for later retrieval using that key. The key names are arbitrary, so as long you use the same name to retrieve the data as when you unarchived it, you can specify any key you like. The only time a conflict might arise is if the same key is used for a subclass of an object being encoded. To prevent this from happening, you can insert the class name in front of the instance variable name when composing the key for the archive, as was done in Program 19.9.

You use the method encodeInt:forKey: instead of encodeValueOfObjCType:at:, which you need to use for unkeyed archives. Table 19.1 depicts the various encoding and decoding methods you can use for keyed archives.

The process of decoding keyed objects is straightforward: You use decodeObject:forKey: for Objective-C objects and the appropriate method from Table 19.1 for basic data types.

Table 19.1. Encoding and Decoding Basic Data Types in Keyed Archives

image

Some of the basic data types, such as char, short, long, and long long, are not listed in Table 19.1. You'll have to determine the size of your data object and use the appropriate routine. For example, a short int is normally 16 bits, an int and long are 32 bits, and a long long is 64 bits. (You can use the sizeof operator as described in Chapter 13, “Underlying C Language Features,” to determine the size of any data type.) So, to archive a short int, store it in an int first and then archive it with encodeInt:forKey:. Reverse the process to get it back: Use decodeInt:forKey: and then assign it to your short int variable.

The test program and output from the keyed archiving example is shown in Program 19.9.

Program 19.9. Test Program


#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSCoder.h>


int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  Foo *myFoo1 = [[Foo alloc] init];
  Foo *myFoo2;

  // First set and archive myFoo1 to a file
  [myFoo1 setAll: @"This is the string" iVal: 12345 fVal: 98.6];
  [NSKeyedArchiver archiveRootObject: myFoo1 toFile: @"foo.karch"];

  // Now restore the archive into myFoo2
  myFoo2 = [NSKeyedUnarchiver unarchiveObjectWithFile:
                       @"foo.karch"];
  printf ("%s %i %g ", [[myFoo2 strVal] cString],
         [myFoo2 intVal], [myFoo2 floatVal]);

  [myFoo1 release];
  [pool release];
  return 0;
}


Program 19.9. Output


This is the string
12345
98.6


Using NSData to Create Custom Archives

You might not want to write your object directly to a file using the archiveRootObject:ToFile: method, as was done in the previous program examples. For example, perhaps you want to collect some or all of your objects and store them in a single archive file. This can be done in Objective-C using the general data stream object class called NSData, which we briefly visited in Chapter 16.

As mentioned in Chapter 16, an NSData object can be used to reserve an area of memory into which you can store data. Typical uses of this data area might be as temporary storage for data that will subsequently be written to a file or perhaps to hold the contents of a file read from the disk. The simplest way to create a mutable data area is with the data method:

dataArea = [NSMutableData data];

This creates an empty buffer space whose size expands as needed as the program executes.

As a simple example, let's assume you want to archive your address book and one of your Foo objects in the same file. Assume for this example that you've added keyed archiving methods to the AddressBook and AddressCard classes (see Program 19.10). If you haven't, or keyed archives aren't supported on your system, you can modify this example to work without keyed archives.

Program 19.10.


#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSCoder.h>
#import <Foundation/NSData.h>
#import "AddressBook.h"
#import "Foo.h"


int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  Foo               *myFoo1 = [[Foo alloc] init];
  Foo               *myFoo2;
  NSMutableData     *dataArea;
  NSKeyedArchiver   *archiver;
  AddressBook       *myBook;

  // Insert code from Program 19.6 to create an Address Book
  // in myBook containing four address cards

  [myFoo1 setAll: @"This is the string" iVal: 12345 fVal: 98.6];

  // Set up a data area and connect it to an NSKeyedArchiver object
  dataArea = [NSMutableData data];

  archiver = [[NSKeyedArchiver alloc]
             initForWritingWithMutableData: dataArea];
  // Now we can begin to archive objects
  [archiver encodeObject: myBook forKey: @"myaddrbook"];
  [archiver encodeObject: myFoo1 forKey: @"myfoo1"];
  [archiver finishEncoding];

  // Write the archived data are to a file
  if ( [dataArea writeToFile: @"myArchive" atomically: YES] == NO)
       printf ("Archiving failed! ");

  [archiver release];

  [myFoo1 release];
  [pool release];
  return 0;
}


After allocating an NSKeyedArchiver object, the initForWritingWithMutableData: message is sent to specify the area in which to write the archived data; this is the NSMutabledata area dataArea you previously created. The NSKeyedArchiver object stored in archiver can now be sent encoding messages to archive objects in your program. In fact, all encoding messages up until it receives a finishEncoding message are archived and stored in the specified data area.

You have two objects to encode here—the first is your address book and the second is your Foo object. You can use encodeObject: for these objects because you have previously implemented encoder and decode methods for the AddressBook, AddressCard, and Foo classes. It's important that you understand that concept.

When you are done archiving your two objects, you send the archiver object the finishEncoding message. No more objects can be encoded after that point, and you need to send this message to complete the archiving process.

The area you set aside and named dataArea now contains your archived objects in a form you can write to a file. The message expression

[data writeToFile: @"myArchive" atomically: YES]

sends the writeToFile:atomically: message to your data stream to ask it to write its data to the specified file, which you named myArchive.

As you can see from the if statement, the writeToFile:atomically: method returns a BOOL value: YES if the write operation succeeds and NO if it fails (perhaps an invalid pathname for the file was specified or the file system is full).

Restoring the data from your archive file is simple—you just do things in reverse. First, you need to allocate a data area like before. Next, you need to read your archive file into the data area, and then you have to create an NSKeyedUnarchiver object and tell it to decode data from the specified area. You must invoke decode methods to extract and decode your archived objects. When you're all done, you send a finishDecoding message to the NSKeyedUnarchiver object.

This is all done in Program 19.11 that follows.

Program 19.11.


#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSCoder.h>
#import <Foundation/NSData.h>
#import "AddressBook.h"
#import "Foo.h"

int main (int argc, char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSData            *dataArea;
  NSKeyedUnarchiver *unarchiver;
  Foo               *myFoo1;
  AddressBook       *myBook;
  // Read in the archive and connect an
  // NSKeyedUnarchiver object to it

  dataArea = [NSData dataWithContentsOfFile: @"myArchive"];
  unarchiver = [[NSKeyedUnarchiver alloc]
                        initForReadingWithData: dataArea];

  // Decode the objects we previously stored in the archive
  myBook = [unarchiver decodeObjectForKey: @"myaddrbook"];
  myFoo1 = [unarchiver decodeObjectForKey: @"myfoo1"];

  [unarchiver finishDecoding];

  [unarchiver release];

  // Verify that the restore was successful
  [myBook list];
   printf ("%s %i %g ", [[myFoo1 strVal] cString],
           [myFoo1 intVal], [myFoo1 floatVal]);

  [pool release];
  return 0;
}


Program 19.11. Output


======== Contents of: Steve's Address Book =========
Jamie Baker       [email protected]
Julia Kochan      [email protected]
Stephen Kochan     [email protected]
Tony Iannino       [email protected]
====================================================

This is the string
12345
98.6


The output verifies that the address book and your Foo object were successfully restored from the archive file.

Using the Archiver to Copy Objects

In Program 19.2, you tried to make a copy of an array containing mutable string elements and saw how a shallow copy of the array was made. That is, the actual strings themselves were not copied, only the references to them.

You can use the Foundation's archiving capabilities to create a deep copy of an object. For example, Program 19.12 copies dataArray to dataArray2 by archiving dataArray into a buffer and then unarchiving it, assigning the result to dataArray2. You don't need to use a file for this process; the archiving and unarchiving process can all take place in memory.

Program 19.12.


#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSArray.h>

int main (int argc, char *argv[])
{
   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   NSData       *data;
   NSMutableArray  *dataArray = [NSMutableArray arrayWithObjects:
        [NSMutableString stringWithString: @"one"],
        [NSMutableString stringWithString: @"two"],
        [NSMutableString stringWithString: @"three"],
        nil
    ];

   NSMutableArray   *dataArray2;
   NSMutableString  *mStr;
   int              i, n;

  // Make a deep copy using the archiver

  data = [NSArchiver archivedDataWithRootObject: dataArray];
  dataArray2 = [NSUnarchiver unarchiveObjectWithData: data];

  mStr = [dataArray2 objectAtIndex: 0];
  [mStr appendString: @"ONE"];

  printf (" dataArray: ");
  for (i = 0; i < [dataArray count]; ++i)
      printf ("%s ", [[dataArray objectAtIndex: i] cString]);

  printf (" dataArray2: ");
  n = [dataArray2 count];
  for (i = 0; i < n; ++i)
      printf ("%s ", [[dataArray2 objectAtIndex: i] cString]);

  printf (" ");
  [pool release];
  return 0;
}


Program 19.12. Output


dataArray: one two three
dataArray2: oneONE two three


The output verifies that changing the first element of dataArray2 had no effect on the first element of dataArray. That's because a new copy of the strings was made through the archiving/unarchiving process.

The copy operation in Program 19.12 is performed with the following two lines:

data = [NSArchiver archivedDataWithRootObject: dataArray];
dataArray2 = [NSUnarchiver unarchiveObjectWithData: data];

You can even avoid the intermediate assignment and perform the copy with a single statement like this:

dataArray2 = [NSUnarchiver unarchiveObjectWithData:
                [NSArchiver archivedDataWithRootObject: dataArray]];

This is a technique you might want to keep in mind next time you need to make a deep copy of an object or of an object that doesn't support the NSCopying protocol.

Exercises

  1. In Chapter 15, Program 15.8 generated a table of prime numbers. Modify that program to write the resulting array as a property list to the file primes.pl. Then, examine the contents of the file.
  2. Write a program to read in the property list created in exercise 1 and store the values in an array object. Print all the elements of the array to verify that the restore operation was successful.
  3. The glossary in Appendix A has been stored online as a traditional property list at the URL http://www.kochan-wood.com/examples/glossary.pl. Write a program to read the glossary into a dictionary object and then display its contents.
  4. Modify the program developed in exercise 3 to look up an entry in the glossary based on a term entered on the command line. So

    $ glossary object

    should display the meaning of “object” as defined in the glossary. Be sure to handle the cases where a term can't be found or if the term consists of more than one word, like this:

    $ glossary instance method

    This should look up the meaning of the term “instance method” in the glossary.

  5. Write a program to read in an archived AddressBook and look up an entry based on a name supplied on the command line, like so:

    $ lookup gregory

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

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