Chapter 8. Archiving

While an object-oriented program is running, a complex graph of objects is being created. It is often necessary to represent this graph of objects as a stream of bytes, a process called archiving (Figure 8.1). This stream of bytes can then be sent across a network connection or written into a file. For example, when you save a nib file, Interface Builder is archiving objects into a file. (Instead of “archiving,” a Java programmer would call this process “serialization.”)

Archiving

Figure 8.1. Archiving

When you need to recreate the graph of objects from the stream of bytes, you will unarchive it. For example, when your application starts up, it unarchives the objects from the nib file created by Interface Builder.

Although objects have both instance variables and methods, only the instance variables and the name of the class go into the archive. In other words, only data goes into the archive, not code. As a result, if one application archives an object and another application unarchives the same object, both applications must have the code for the class linked in. In the nib file, for example, you have used classes like NSWindow and NSButton from the AppKit framework. If you do not link your application against the AppKit framework, it will be unable to create the instances of NSWindow and NSButton that it finds in the archive.

There was once a shampoo ad that said, “I told two friends, and they told two friends, and they told two friends, and so on, and so on, and so on.” The implication was that as long as you told your friends about the shampoo, everyone who matters would eventually wind up using the shampoo. Object archiving works in much the same way. You archive a root object, it archives the objects to which it is attached, they archive the objects to which they are attached, and so on, and so on, and so on. Eventually, every object that matters will be in the archive.

Archiving involves two steps. First, you need to teach your objects how to archive themselves. Second, you need to cause the archiving to occur.

The Objective-C language has a construct called a protocol, which is identical to the Java construct called an interface. That is, a protocol is a list of method declarations. When you create a class that implements a protocol, it promises to implement all the methods declared in the protocol.

NSCoder and NSCoding

One protocol is called NSCoding. If your class implements NSCoding, it promises to implement the following methods:

- (id)initWithCoder:(NSCoder *)coder;

- (void)encodeWithCoder:(NSCoder *)coder;

An NSCoder is an abstraction of a stream of bytes. You can write your data to a coder or read your data from a coder. The initWithCoder: method in your object will read data from the coder and save that data to its instance variables. The encodeWithCoder: method in your object will read its instance variables and write those values to the coder. In this chapter, you will implement both methods in your Person class.

NSCoder is actually an abstract class. You won't ever create instances of an abstract class. Instead, an abstract class has some capabilities that are intended to be inherited by subclasses. You will create instances of the concrete subclasses. Namely, you will use NSKeyedUnarchiver to read objects from a stream of data, and you will use NSKeyedArchiver to write objects to the stream of data.

Encoding

NSCoder has many methods, but most programmers find themselves using just a few of them repeatedly. Here are the methods most commonly used when you are encoding data onto the coder:

- (void)encodeObject:(id)anObject forKey:(NSString *)aKey

This method writes anObject to the coder and associates it with the key aKey. This will cause anObject's encodeWithCoder: method to be called (and they told two friends, and they told two friends…).

For each of the common C primitive types (like int and float), NSCoder has an encode method:

- (void)encodeBool:(BOOL)boolv forKey:(NSString *)key

- (void)encodeDouble:(double)realv forKey:(NSString *)key

- (void)encodeFloat:(float)realv forKey:(NSString *)key

- (void)encodeInt:(int)intv forKey:(NSString *)key

To add encoding to your Person class, add the following method to Person.m:

- (void)encodeWithCoder:(NSCoder *)coder
{
    [coder encodeObject:personName forKey:@"personName"];
    [coder encodeFloat:expectedRaise forKey:@"expectedRaise"];
}

If you looked at the documentation for NSString, you would see that it implements the NSCoding protocol. Thus, the personName knows how to encode itself.

All of the commonly used AppKit and Foundation classes implement the NSCoding protocol, with the notable exception of NSObject. Because Person inherits from NSObject, it doesn't call [super encodeWithCoder:coder]. If Person's superclass had implemented the NSCoding protocol, the method would have looked like this:

- (void)encodeWithCoder:(NSCoder *)coder
{
    [super encodeWithCoder:coder];
    [coder encodeObject:personName forKey:@"personName"];
    [coder encodeFloat:expectedRaise forKey:@"expectedRaise"];
}

The call to the superclass's encodeWithCoder: method would give the superclass a chance to write its variables onto the coder. Thus, each class in the hierarchy writes only its instance variables (and not its superclass's instance variables) onto the coder.

Decoding

When decoding data from the coder, you will use the analogous decoding methods:

- (id)decodeObjectForKey:(NSString *)aKey

- (BOOL)decodeBoolForKey:(NSString *)key

- (double)decodeDoubleForKey:(NSString *)key

- (float)decodeFloatForKey:(NSString *)key

- (int)decodeIntForKey:(NSString *)key

If, for some reason, the stream does not include the data for a key, you will get zero for the result. For example, if the object did not write out data for the key “foo” when the stream was first written, the coder will return if it is later asked to decode a float for the key “foo”. If the coder is asked to decode an object for the key “foo”, it will return nil.

To add decoding to your Person class, add the following method to your Person.m file:

- (id)initWithCoder:(NSCoder *)coder
{
   [super init];
   [self setPersonName:[coder decodeObjectForKey:@"personName"]];
   [self setExpectedRaise:[coder decodeFloatForKey:@"expectedRaise"]];
   return self;
}

Once again, you did not call the superclass's implementation of initWithCoder:, because NSObject doesn't have one. If Person's superclass had implemented the NSCoding protocol, the method would have looked like this:

- (id)initWithCoder:(NSCoder *)coder
{
  [super initWithCoder:coder];
  [self setPersonName:[coder decodeObjectForKey:@"personName"]];
  [self setExpectedRaise:[coder decodeObjectForKey:@"expectedRaise"]];
  return self;
}

The attentive reader may now be saying, “Chapter 3 said that the designated initializer does all the work and calls the superclass's designated initializer. It said that all other initializers call the designated initializer. But Person has an init method, which is its designated initializer, and this new initializer doesn't call it.” You are right: initWithCoder: is an exception to initializer rules.

You have now implemented the methods in the NSCoding protocol. To declare your Person class as implementing the NSCoding protocol, you will edit the Person.h file. Change the declaration of your class to look like this:

@interface Person : NSObject <NSCoding> {

Now try to compile the project. Fix any errors. You could run the application at this point, if you like. However, although you have taught Person objects to encode themselves, you haven't asked them to do so. Thus, you will see no change in the behavior of your application.

The Document Architecture

Applications that deal with multiple documents have a lot in common. All of them can create new documents, open existing documents, save or print open documents, and remind the user to save edited documents when he tries to close a window or quit the application. Apple supplies three classes that take care of most of the details for you: NSDocumentController, NSDocument, and NSWindowController. Together, these three classes constitute the document architecture.

The purpose of the document architecture relates to the Model-View-Controller design pattern discussed in Chapter 6. In RaiseMan, your subclass of NSDocument (with the help of NSArrayController) acts as the controller. It will have a pointer to the model objects, and will be responsible for the following duties:

  • Saving the model data to a file

  • Loading the model data from a file

  • Displaying the model data in the views

  • Taking user input from the views and updating the model

Info.plist and NSDocumentController

When Xcode builds an application, it includes a file called Info.plist. (Later in this chapter, you will change Info.plist.) When the application is launched, it reads from Info.plist, which tells it what type of files it works with. If it finds that it is a document-based application, it creates an instance of NSDocumentController (Figure 8.2). You will seldom have to deal with the document controller; it lurks in the background and takes care of a bunch of details for you. For example, when you choose the New or Save All menu item, the document controller handles the request. If you need to send messages to the document controller, you could get to it like this:

NSDocumentController *dc;
dc = [NSDocumentController sharedDocumentController];
Document Controller

Figure 8.2. Document Controller

The document controller has an array of document objects—one for each open document.

NSDocument

The document objects are instances of a subclass of NSDocument. In your RaiseMan application, for example, the document objects are instances of MyDocument. For many applications, you can simply extend NSDocument to do what you want; you don't have to worry about NSDocumentController or NSWindowController at all.

Saving

The menu items Save, Save As…, Save All, and Close are all different, but all deal with the same problem: getting the model into a file or file wrapper. (A file wrapper is a directory that looks like a file to the user.) To handle these menu items, your NSDocument subclass must implement one of three methods:

  • - (NSData *)dataRepresentationOfType:(NSString *)aType
    
  • Your document object supplies the model to go into the file as an NSData object. NSData is essentially a buffer of bytes. It is the easiest and most popular way to implement saving in a document-based application. Return nil if you are unable to create the data object and the user will get an alert sheet indicating that the save attempt failed. Notice that you are passed the type, which allows you to save the document in one of several possible formats. For example, if you wrote a graphics program, you might allow the user to save the image as a gif or a jpg file. When you are creating the data object, aType indicates the format that the user has requested for saving the document. If you are dealing with only one type of data, you may simply ignore aType.

    - (NSFileWrapper *)fileWrapperRepresentationOfType:
                                                  (NSString *)aType
    
  • Your document object returns the model as an NSFileWrapper object. It will be written to the filesystem in the location chosen by the user.

    - (BOOL)writeToFile:(NSString *)filename
                 ofType:(NSString *)type
    
  • Your document object is given the filename and the type. It is responsible for getting the model into the file. Return YES if the save is successful and NO if the save fails.

Loading

The Open..., Open Recent, and Revert To Saved menu items, although different, all deal with the same basic problem: getting the model from a file or file wrapper. To handle these menu items, your NSDocument subclass must implement one of three methods:

  • - (BOOL)loadDataRepresentation:(NSData *)docData
                             ofType:(NSString *)docType
    
  • Your document is passed an NSData object that holds the contents of the file that the user is trying to open. Return YES if you successfully create a model from the data. If you return NO, the user will get an alert panel telling him or her that the application was unable to read the file.

    - (BOOL)loadFileWrapperRepresentation:(NSFileWrapper *)wrapper
                                    ofType:(NSString *)docType
    
  • Your document reads the data from an NSFileWrapper object.

    - (BOOL)readFromFile:(NSString *)filename
                  ofType:(NSString *)docType
    
  • Your document object is passed the path. The document reads the data from the file.

After implementing one save method and one load method, your document will know how to read from and write to files. When opening a file, the document will read the document file before reading the nib file. As a consequence, you will not be able to send messages to the user interface objects immediately after loading the file (because they won't exist yet). To solve this problem, after the nib file is read, your document object is sent the following method:

- (void)windowControllerDidLoadNib:(NSWindowController *)x;

In your NSDocument subclass, you will implement this method to update the user interface objects as you did in Chapter 4.

If the user chooses Revert To Saved from the menu, the model is loaded but windowControllerDidLoadNib: is not called. You will, therefore, also have to update the user interface objects in the method that loads the data, just in case it was a revert operation. One common way to deal with this possibility is to check one of the outlets set in the nib file. If it is nil, the nib file has not been loaded and there is no need to update the user interface.

NSWindowController

The final class in the document architecture that we might discuss would be NSWindowController, but you will not initially need to worry about it. For each window that a document opens, it will typically create an instance of NSWindowController. As most applications have only one window per document, the default behavior of the window controller is usually perfect. Nevertheless, you might want to create a custom subclass of NSWindowController. in the following situations:

  • You need to have more than one window on the same document. For example, in a CAD program you might have a window of text that describes the solid and another window that shows a rendering of the solid.

  • You want to put the user interface controller logic and model controller logic into separate classes.

  • You want to create a window without a corresponding NSDocument object. You will do this in Chapter 9.

Saving and NSKeyedArchiver

Now that you have taught your object to encode and decode itself, you will use it to add saving and loading to your application. When it is time to save your people to a file, your MyDocument class will be asked to create an instance of NSData. Once your object has created and returned an NSData object, it will be automatically written to a file.

To create an NSData object, you will use the NSKeyedArchiver class. NSKeyedarchiver has the following class method:

+ (NSData *)archivedDataWithRootObject:(id)rootObject

This method archives the objects into the NSData object's buffer of bytes.

Once again, we return to the idea of “I told two friends, and they told two friends.” When you encode an object, it will encode its objects, and they will encode their objects, and so on, and so on, and so on. What you will encode, then, is the employees array. It will encode the Person objects to which it has references. Each Person object (because you implemented encodeWithCoder:) will, in turn, encode the personName string and the expectedRaise float.

To add saving capabilities to your application, edit the method dataRepresentationOfType: so that it looks like this:

- (NSData *)dataRepresentationOfType:(NSString *)aType
{
    // End editing
    [personController commitEditing];
    // Create an NSData object from the employees array
    return [NSKeyedArchiver archivedDataWithRootObject:employees];
}

Loading and NSKeyedUnarchiver

Now you will add the ability to load files to your application. Once again, NSDocument has taken care of most of the details for you.

To do the unarchiving, you will use NSKeyedUnarchiver. NSKeyedUnarchiver has the following handy method:

+ (id)unarchiveObjectWithData:(NSData *)data

In your MyDocument class, edit your loadDataRepresentation:ofType: method to look like this:

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
    NSLog(@"About to read data of type %@", aType);
    NSMutableArray *newArray;
    newArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];

    if (newArray == nil) {
        return NO;
    } else {
        [self setEmployees:newArray];
        return YES;
    }
}

You could update the user interface after the nib file is loaded, but NSArrayController will handle it for you—the windowControllerDidLoadNib: method doesn't need to do anything. Leave it here for now because you will add to it in Chapter 10:

- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
    [super windowControllerDidLoadNib:aController];
}

Note that your document is asked which nib file to load when a document is opened or created. This method also needs no changing:

- (NSString *)windowNibName
{
    return @"MyDocument";
}

The window is automatically marked as edited when you make an edit, because you have properly enabled the undo mechanism. When you register your changes with the undo manager for this document, it will automatically mark the document as edited.

At this point, your application can read and write to files. Compile your application and try it out. Everything should work correctly, but all your files will have the extension “????”. You need to define an extension for your application in the Info.plist.

Setting the Extension and Icon for the File Type

RaiseMan files will have the extension .rsmn, and .rsmn files will have an icon. First, find an .icns file and copy it into your project. A fine icon is found at /Developer/Examples/AppKit/CompositeLab/BBall.icns. Drag it from the Finder into the Groups and Files view of Xcode. Drop it in the Resources group (Figure 8.3).

Drag Icon into Project

Figure 8.3. Drag Icon into Project

Xcode will bring up a sheet. Make sure that you check Copy items into destination group's folder (Figure 8.4). This will copy the icon file into your project directory.

Make It a Copy

Figure 8.4. Make It a Copy

To set the document-type information, select the RaiseMan target in Xcode and open the info panel by choosing Show Inspector from the Project menu. Under the Properties tab, set the identifier for your application to be com.bignerdranch.RaiseMan. Set the Icon File to be BBall.icns. In the document types table view, set the name to be RaiseMan File. Set the Extensions to be rsmn. Set the icon for the file type to be BBall.icns. (You'll need to scroll the table to the right to see the Icon File column). See Figure 8.5.

Specify Icon and Document Types

Figure 8.5. Specify Icon and Document Types

Note that Xcode does incremental builds: Only edited files are recompiled. To accomplish this, it maintains many intermediate files between builds. To remove these intermediate files, you can clean the project by choosing Clean from the Build menu. In my experience, the changes that you have just made don't seem to take unless the project is cleaned and rebuilt.

Clean, build, and run your application. You should be able to save data to a file and read it in again. In Finder, the BBall.icns icon will be used as the icon for your .rsmn files.

An application is actually a directory. The directory contains the nib files, images, sounds, and executable code for the application. In Terminal, try the following:

> cd /Applications/TextEdit.app/Contents
> ls

You will see three interesting things.

  • The Info.plist file, which includes the information about the application, its file types, and associated icons. Finder uses this information.

  • The MacOS/ directory, which contains the executable code.

  • The Resources/ directory, which has the images, sounds, and nib files that the application uses. You will see localized resources for several different languages.

For the More Curious: Preventing Infinite Loops

The astute reader may be wondering: “If object A causes object B to be encoded, and object B causes object C to be encoded, and then object C causes object A to be encoded again, couldn't it just go around and around in an infinite loop?” It would, but NSKeyedArchiver was designed with this possibility in mind.

When an object is encoded, a unique token is also put onto the stream. Once archived, the object is added to the table of encoded objects under that token. When NSKeyedArchiver is told to encode the same object again, it simply puts a token in the stream.

When NSKeyedUnarchiver decodes an object from the stream, it puts both the object and its token in a table. The unarchiver finds a token with no associated data, so it knows to look up the object in the table instead of creating a new instance.

This idea led to the method in NSCoder that often confuses developers when they read the documentation:

- (void)encodeConditionalObject:(id)anObject forKey:(NSString *)aKey

This method is used when object A has a pointer to object B, but object A doesn't really care if B is archived. However, if another object has archived B, A would like the token for B put into the stream. If no other object has archived B, it will be treated like nil.

For example, if you were writing an encodeWithCoder: method for an Engine object (Figure 8.6), it might have an instance variable called car that is a pointer to the Car object that it is part of. If you are just archiving the Engine, you wouldn't want the entire Car archived. But if you were archiving the entire Car, you would want the car pointer set. In this case, you would make the Engine object encode the car pointer conditionally.

Conditional Encoding Example

Figure 8.6. Conditional Encoding Example

For the More Curious: Versioning

As your application evolves, instance variables may be added and removed from your classes. The stream created by one version of your application may not be the same as the stream created by another version. How will you deal with this situation?

When an object is encoded, the name of the class and its version are added to the stream. To enable the developer to access this information, NSCoder has the following method:

- (unsigned)versionForClassName:(NSString *)className

Imagine that version 2 of the class Person had an instance variable called phone of type NSString but that version 1 did not. You could create a method like this:

- (id)initWithCoder:(NSCoder *)coder
{
    unsigned version;
    if (self = [super init]) {
        version = [coder versionForClassName:@"Person"];
        [self setPersonName:[coder decodeObjectForKey:@"personName"]];
        [self setExpectedRaise:
                         [coder decodeObjectForKey:@"expectedRaise"]];
        if (version > 1)
            [self setPhone:[coder decodeObjectForKey:@"phoneNumber"]];
        else
            // Set the phone number to a default value
            [self setPhone:@"(555)555-5555"];
    }
    return self;
}

How do you set the version of your class before it is encoded? NSObject declares the class method:

+ (void)setVersion:(int)theVersion

By default, the version is 0.

If you are using versioning, be sure to call setVersion: before any instances are encoded. One easy way to do so is via the class's initialize method. Just as init is sent to an instance to make sure that it is prepared for use, initialize is sent to a class to make sure that it is prepared for use. That is, before a class is used, it is automatically sent the message initialize.

A subclass inherits this method from its superclass. Thus, if you want the code executed only for this class (and not its subclasses), you will prefix it with an if statement:

+ (void)initialize
{
    // Am I the Person class?
    if (self == [Person class]) {
        [self setVersion:2];
    }
}

For the More Curious: Creating a Protocol

Creating your own protocol is very simple. Here is a protocol with two methods. It would typically be in a file called Foo.h.

@protocol Foo
- (void)bar:(int)x;
- (float)baz;
@end

If you had a class that wanted to implement the Foo protocol and the NSCoding protocol, it would look like this:

#import "Rex.h"
#import "Foo.h"

@interface Fido:Rex <Foo, NSCoding>
...etc...
@end

A class doesn't have to redeclare any method it inherits from its superclass, nor does it have to redeclare any of the methods from the protocols it implements. Thus, in our example, the interface file for the class Fido is not required to list any of the methods in Rex or Foo or NSCoding.

For the More Curious: Document-Based Applications Without Undo

The NSUndoManager for your application knows when unsaved edits have occurred. Also, the window is automatically marked as edited. But what if you've written an application and you are not registering your changes with the undo manager?

NSDocument keeps track of how many changes have been made. It has a method for this purpose:

- (void)updateChangeCount:(NSDocumentChangeType)change;

The NSDocumentChangeType can be one of the following: NSChangeDone, NSChangeUndone, or NSChangeCleared. NSChangeDone increments the change count, NSChangeUndone decrements the change count, and NSChangeCleared sets the change count to 0. The window is marked as dirty unless the change count is 0.

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

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