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.”)
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.
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.
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.
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.
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
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];
The document controller has an array of document objects—one for each open document.
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.
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.
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.
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.
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]; }
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:
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
.
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).
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.
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.
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.
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.
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]; } }
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
.
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.