| [ Team LiB ] |
|
Writing Encoding and Decoding MethodsBasic 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 = @"jewls337@axlc.com";
NSString *bName = @"Tony Iannino";
NSString *bEmail = @"tony.iannino@techfitness.com";
NSString *cName = @"Stephen Kochan";
NSString *cEmail = @"steve@kochan-wood.com";
NSString *dName = @"Jamie Baker";
NSString *dEmail = @"jbaker@kochan-wood.com";
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\n");
[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 jbaker@kochan-wood.com Julia Kochan jewls337@axlc.com Stephen Kochan steve@kochan-wood.com Tony Iannino tony.iannino@techfitness.com ==================================================== 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 TypesThe 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\n%i\n%g\n", [[myFoo2 strVal] cString],
[myFoo2 intVal], [myFoo2 floatVal]);
[myFoo1 release];
[pool release];
return 0;
}
Program 19.8 OutputThis is the string 12345 98.6 |
| [ Team LiB ] |
|