So far, our examples have centered on applications that have a single GUI. However, in reality many of the applications we use day-in and day-out—such as word processors and web browsers—are based around the idea of a document. They provide a framework for viewing or generating identically-contained, but uniquely-composed, sets of data that can be stored in files.
A document-based application must perform the following tasks:
Create new documents
Open existing documents stored in files
Save documents to user-designated files and locations
Revert to previously saved documents
Close documents, usually after prompting the user to save changes
Print documents and allow the page layout to be modified
Monitor and set the document’s edited status, as well as reflect that status to the user
Manage document windows, including setting window titles
Cocoa provides a multiple-document architecture, helping you take care of these tasks easily. Using this architecture drastically simplifies the work developers must do to implement a multidocument application. Once you understand how this architecture works, you can have a multidocument application up and running in minutes.
This chapter begins with an overview of Cocoa’s multiple-document architecture and then presents an in-depth look at the classes that make up this architecture. The final part of the chapter guides you through the process of creating a simple multiple-document text-editing application.
From a user’s perspective, a document is a unique body of information contained in its own window. The window gives the user an area in which to edit the document. Users can create an unlimited number of documents and save each to a separate file.
From a Cocoa programming perspective, a document is managed by an
instance of the
NSDocument
class, which, along with
NSDocumentController
and
NSWindowController
, provides the functionality for a
document-based application. Objects of these classes divide and
orchestrate the work of creating, saving, opening, and managing the
documents that an application creates. They are tiered in a
one-to-many relationship, as depicted in Figure 10-1.
Document-based applications have one instance of the
NSDocumentController
class, which creates and
manages many potential NSDocument
objects (one for
each new or open document). In turn, an NSDocument
object creates and manages one or more
NSWindowController
objects, one for each of the
windows displayed for a document.
In addition to these three AppKit classes, the multiple-document architecture uses information in the application’s info property list (saved as Contents/Info.plist in the application’s bundle — we’ll discuss bundles more in Chapter 13) to determine the types of data with which the application can work. The information is stored in the property list as an array of document types. Each document-type entry in the array includes the following information:
The name of the document type.
An array of filename extensions, such as .rtf and .txt, which correspond to a document’s data type.
An array of Mac OS-style type identifiers, such as
TEXT
and PICT
, which also
correspond to a document’s data type.
A string that determines the role of the application when interacting with data. An application can be an Editor or a View for a given type.
The class name of the NSDocument
subclass that
handles the data type in your application.
Project Builder provides a simple user interface for creating and editing entries in an application’s document type array. Even though there’s usually no need to modify the property list directly, the document controller uses the information from the info property list to do the following things:
Filter out inappropriate file types automatically, allowing users to select only files that the application can handle when an open dialog box is presented
Instantiate the appropriate NSDocument
subclass
for a document’s data type when a document is opened
The primary job of a document object—an instance of an
NSDocument
subclass that you provide as part of
your application—is to represent, manipulate, store, and load
the data associated with a document. Based on the document types it
claims to understand (as specified in the
application’s info property list), a document object
must be prepared to do the following things:
Provide other objects in the application that the data displayed in its window(s). The document object must provide the data in any of the formats supported by the application.
Load data into internal data structures and display it in windows. The document object must accept the data in any format supported by the application.
Store document data in a file at a specified location in the filesystem.
Read document data stored in a file.
With the assistance of its window controllers, a document-object
instance manages the display and capture of the data in its windows.
The document-object instance associated with the key window is made
the first responder to action messages indicating that a user wants
to save, print, revert, or close a document. A fully implemented
document object knows how to track its edited status, print document
data, and perform undo and redo operations. As
you’ll see in the examples in this and later
chapters, these behaviors aren’t provided completely
by default, but the NSDocument
class goes a long
way to assist you in implementing each.
For edited-status tracking, the NSDocument
class
provides an API for updating a document change counter. For undo/redo
operations, NSDocument
creates an
NSUndoManager
when one is requested, which
responds appropriately to the Edit → Undo and Edit
→ Redo menu commands, updating the change counter
when undo and redo operations are invoked.
Every application that takes advantage of the
AppKit’s document-based
application architecture must create at least one subclass of
NSDocument
. The architecture requires that you
override some methods of the NSDocument
class.
These methods must be implemented:
- (NSString *)windowNibName;
Called by the document controller to determine the name of the nib file that contains the user interface to view and edit the document.
- (void)windowControllerDidLoadNib:(NSWindowController *)aController;
Called once the window controller has loaded the nib file and all of the user interface connections have been made. This provides an opportunity for any initialization that needs to be performed.
- (NSData *)dataRepresentationOfType:(NSString *)aType;
Must be implemented to create and return document data of a supported
type, usually in preparation for writing that data to a file as an
NSData
object.
- (BOOL)loadDataRepresentation:(NSData *)data:(NSString *)aType;
Must be implemented to convert an NSData
object
(that contains the document data of a particular type) into the
document’s internal data structures so that the
document is ready to display its contents. The
NSData
object usually results from the document
reading a document file.
The primary job of an
application’s document-controller object
(NSDocumentController
) is to create and open
documents, as well as to track and manage
these documents. The document controller maintains a list of document
objects and tracks the current document (the document whose window is
currently key). It is hardwired to respond appropriately to certain
application events, such as when the application starts up, when it
terminates, when the system powers off, and when documents are opened
or printed from the Finder. For example, when a user chooses New from
the File menu, the document controller does the following things:
Allocates an instance of the NSDocument
subclass
specified in the first entry of the application’s
document type array
Initializes the instance by invoking the subclass’s
init
method
When the user chooses Open from the File menu, the document controller does the following things:
Displays the Open panel, filtering the file list using the data type(s) from the application’s info property list, and gets the user’s selection
Uses the type information from the file and data to allocate an
instance of the appropriate NSDocument
subclass
Initializes the object by invoking its
initWithContentsOfFile:ofType:
method, which loads
the contents of the file into the document instance
When the user chooses Save or Save As from the File menu, the document controller does the following things:
If needed (if the document has not been saved before, or if the user chooses Save As), displays the Save panel and gets the user’s selection
Uses the type information from the filename that the user gave and
requests the data from the application using the
dataRepresentationOfType:
method
Stores the data in the returned data object into the filesystem
In a document-based application, many of the
application’s menu items are already connected to the
document controller. These methods are implemented by the
NSDocumentController
class and are listed in Table 10-1.
The default document-controller behavior provided by the
NSDocumentController
class is usually sufficient
for most situations; you shouldn’t need to subclass
it unless you need to provide alternative functionality for the
methods listed earlier.
A
window
controller, an instance of the
NSWindowController
class, manages one
window associated with a document. If a
document has multiple open windows, each window has its own instance
of NSWindowController
. For example, a document
might have a main data-entry window and a window that lists records
for selection. Each window would have its own window controller. When
a document has multiple window controllers, only one of them is
considered the primary window controller. When the primary window is
closed, the document and all other windows are closed.
When requested by the
NSDocument
class, a window controller loads the nib
file containing a window and displays it. The window controller
assumes responsibility for managing the nib file.
When a document is closed, the window controller is responsible for properly closing windows, as well as freeing any top-level objects instantiated by the nib file. This includes the window itself and any additional objects added to the nib.
Most of the time, you can use the default window controller provided
by the AppKit. Some applications may want to subclass
NSWindowController
to move the
user-interface-specific logic out of the
NSDocument
subclass. The Sketch sample application
in /Developer/Examples/AppKit uses this
technique. Another situation that would make subclassing desirable is
if you wanted to support multiple views onto a document; for example,
in a 3D modeling application you would want to present various views
of the model.
The multiple-document architecture automates much of the memory management for documents and their associated window and document controllers. One of the document controller’s responsibilities is to ensure that a document is open and using memory only if it has a window open on the screen. When a window closes, it tells its window controller that it is closing. The window controller, in turn, tells its document that it is closing. The document notes that the window controller is closing, removes the window controller from its list of window controllers, and releases it. As this is the only place the window controller is retained, the window controller gets released and deallocated as a result.
It
is possible to put together a document-based application without
writing very much code. If your requirements are minimal, the AppKit
provides you with default window-controller and document-controller
instances. You are left with the task of composing the document
interface, implementing a subclass of NSDocument
,
and adding any other custom classes or behavior required by your
application.
To show how the pieces of the document-based architecture fit together in practice, we will create a very simple text editor. By the time we’re finished with this example, which consists of a relatively small amount of code, we’ll have created an application that—without Cocoa’s help—might have taken days or weeks to construct and debug.
Project Builder provides a template named "Document-based Application” to expedite the development of these kinds of applications. This project type provides the following things:
This nib contains a standard Cocoa application menu bar. The menu items in the File and Edit menus are already connected to the appropriate first responder action methods in the document controller.
This nib file contains a single window to which other UI elements can
be added. A subclass of NSDocument,
named
MyDocument
, has been
created, has an outlet to the document window, and has been made
File’s Owner of the nib file.
NSDocument
subclass implementationThe project includes MyDocument.h and
MyDocument.m files, matching the definition of
the NSDocument
subclass in the
document’s nib file. The MyDocument.m
file contains commented starter implementations of
important methods (called
“stubbed-out” methods) that will
help you implement the functionality needed.
In the Application Settings pane of the Targets display is a simple user interface for modifying the application’s Info.plist file. The provided file contains placeholder values for global application keys, as well as the document type array.
To get started working on building our text editor:
Launch Project Builder, and choose New Project from the File menu (File → New Project).
Select Cocoa Document-based Application from the application type dialog box, as shown in Figure 10-2.
Name the project “Simple Text Edit”, and save it into your ~/LearningCocoa folder.
Double-click on the MyDocument.nib file (located in the Resources folder of the Groups & Files panel in Project Builder), so you can examine the interface in Interface Builder. The nib file is quite simple, as shown in Figure 10-3. There is only a single window with a default text string.
If you select the File’s Owner instance and bring up the Inspector (Tools → Show Info, or Shift-
-I), you’ll
notice in the Attributes pane that File’s Owner is
set to correspond to an instance of MyDocument
.
Also, in the Connections pane, you’ll see an outlet
with a connection to the window.
Switch back to Project Builder, and double-click on the
MainMenu.nib file to open it in Interface
Builder. Click through the menu items with the Connections inspector
open, as shown in Figure 10-4, and notice how many
of the application’s menu items have already been
connected to appropriate first responder action methods. These
methods are implemented by the application’s
document controller (an NSDocumentController
instance).
Return to Project Builder, and open
MyDocument.m, located in the Classes folder of
the Groups & Files pane. Examine the
skeletal
implementation of this
NSDocument
subclass, and you’ll
see that the four methods that must be implemented already have a
skeletal implementation, as shown in Example 10-1.
#import "MyDocument.h" @implementation MyDocument - (id)init { [super init]; if (self) { // Add your subclass-specific initialization here. // If an error occurs here, send a [self dealloc] message and return nil. } return self; } - (NSString *)windowNibName { // Override returning the nib file name of the document // If you need to use a subclass of NSWindowController or if your // document supports multiple NSWindowControllers, you should remove // this method and override -makeWindowControllers instead. return @"MyDocument"; } - (void)windowControllerDidLoadNib:(NSWindowController *) aController { [super windowControllerDidLoadNib:aController]; // Add any code here that need to be executed once the windowController // has loaded the document's window. } - (NSData *)dataRepresentationOfType:(NSString *)aType { // Insert code here to write your document from the given data. // You can also choose to override -fileWrapperRepresentationOfType: // or -writeToFile:ofType: instead. return nil; } - (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType { // Insert code here to read your document from the given data. // You can also choose to override -loadFileWrapperRepresentation:ofType: // or -readFromFile:ofType: instead. return YES; } @end
Save the project (File → Save, or
-S), and then build and run the application (Build → Build and Run, or
-R).
Now you can experiment with the document-based application.
Create new document windows (File → New, or
-N), and close them (File → Close, or
-W).
Next, try saving a document window (File → Save, or
-S). Notice that a dialog box asks you
to select a location in which to save the document. Choose a location
and hit OK. Another dialog box says that the file could not be saved.
This is because the default
dataRepresentationOfType:
method returns
nil
instead of a valid NSData
object, because no default file type has been specified.
Now quit the application (NewApplication → Quit, or
-Q).
Next, we’ll implement the functionality needed to turn this skeleton into a full-blown text editor that allows us to save and open text files.
In this section, you’ll define the look and feel of the application’s document. Just modify the default nib file (created by Project Builder’s template) by adding a text view that will allow the user to view and edit text.
Open MyDocument.nib in Interface Builder, if it isn’t already open.
Remove the default text object that says “Your document contents here.”
Drag an NSTextView
to the window from the
Cocoa-Data views pane of the palette, as shown in Figure 10-5.
Move and resize the text view so that it occupies the entire window, as shown in Figure 10-6.
With the text view selected, bring up the Size pane in the Inspector. Change the Autosizing options so that the view will follow changes in the windows size.
Switch back to Project Builder, open MyDocument.h, and add a declaration for the text view’s outlet by inserting the boldface text shown in Example 10-2.
Save (
-S) MyDocument.h.
Bring Interface Builder to the front, and drag MyDocument.h from Project Builder’s Group & Files listing into the Instances panel of Interface Builder’s MyDocument.nib window. This gives Interface Builder the opportunity to parse the outlet, so you can use it for connections.
In Interface Builder’s Instances pane, Control-drag
a connection from the File’s Owner instance (this is
a proxy for a MyDocument
instance) to the text
view.
Connect the textView
outlet to the view by
clicking on the Connect button in the Info window.
Save (
-S) the nib file.
The Applications Settings pane of the target window allows you to create and modify a variety of application-wide properties. Critical values, like the name of the executable and the name of the main Cocoa class, are provided by default. Many of the other properties are important for a full-fledged application, but they can remain unset for this simple example. You’ll learn more about these properties later in the book. For now, don’t worry about them.
Our Simple Text Edit application will handle only one kind of data: text. It’s very simple to modify the application’s info property list to add support for this document type.
In Project Builder, select the Targets pane in the main window.
Select the default (and only) target named Simple Text Edit.
Select the Info.plist Entries → Simple View → Document Types in the outline, as shown in Figure 10-7.
Modify the default document type entry. Rename DocumentType to Text, and replace the quoted question marks with txt in the Extensions field and with TXT in the OS types field. Once you’ve entered this information, click on the Change button.
These settings allow the document architecture to recognize
.txt files as files that can be opened by our
application, instructing the system to use an instance of the
MyDocument
class to open those files. In addition,
the system will allow only files saved from a
MyDocument
instance to have the extension
.txt.
Now, we
implement the MyDocument
class to support reading
and writing text data.
In Project Builder, click vertical Files tab, then select the MyDocument.h file from the Classes folder in the Groups & Files panel.
Add the dataFromFile
instance variable as shown:
#import <Cocoa/Cocoa.h>
@interface MyDocument : NSDocument
{
IBOutlet NSTextView * textView;
NSData * dataFromFile;
}
@end
This variable will hold a reference to the raw data loaded from a file.
Open MyDocument.m. The following steps will fill in the methods of the skeleton source file from Example 10-1. We’ll fill in the stubbed methods in a different order than they appear in the file so that we can have each step build on top of the previous one. In addition, we’ll show the code without the comments—it’s your choice whether to leave them in your application.
Implement the loadDataRepresentation:
method so
that text data can be loaded from the filesystem into the document.
When a new document is created, this method is called before the nib
is fully loaded and all of the connections have been made. Because of
this, the connection to the text view won’t be made
yet. In this method, we are just going to store the
data
object into the
dataFromFile
variable.
- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
dataFromFile = [data retain];
return YES;
}
Implement the dataRepresentationOfType:
method so
that the document can save its contents. The
NSTextView
class can present its data as a string
that we can encode into a data object.
- (NSData *)dataRepresentationOfType:(NSString *)aType { NSString * text = [textView string]; return [text dataUsingEncoding:NSUTF8StringEncoding]; }
Implement the windowControllerDidLoadNib:
method
so that text data can be loaded into the text view.
- (void)windowControllerDidLoadNib:(NSWindowController *) aController { [super windowControllerDidLoadNib:aController]; if (dataFromFile){ NSString * text = [[NSString alloc]initWithData:dataFromFile // a encoding:NSUTF8StringEncoding]; [textView setString:text]; // b [text release]; } [textView setAllowsUndo:YES]; // c }
The code we added does the following things:
Creates a string from the dataFromFile
object.
Sets the string that serves as the
textView
’s model to the string
that we just created for the dataFromFile
object.
Enables Undo and Redo functionality that is already built into the
NSTextView
class. With this enabled, text changes
can be undone and redone. The Undo Manager can keep an unlimited
number of undos in its stack. As well, the document can keep track of
the edited status of the application.
Add a dealloc
method at the end of the
MyDocument.m file (before the
@end
statement) to clean up the
dataFromFile
object.
- (void)dealloc { [dataFromFile release]; [super dealloc]; }
Save the project (
-S), clean it (Build → Clean),[17] and then build and run the application (Build → Build and Run, or
-R). Try the following:
Type some text into the running application. Use Cut and Paste to edit the text.
Save the document. Note the filename appears in the window’s titlebar. Make sure that the “Hide Extension” checkbox is not clicked so that you can see the extension of the file in the Finder and other applications.
Play with the spell checker.
Close the document window (File → Close, or
-W).
Open the document you saved in step 2 in TextEdit (/Applications) to see how Mac OS X’s default text editor handles the data created by the Simple Text Editor application.
Quit TextEdit.
Cocoa’s multiple-document architecture, as well as
the capabilities built into the NSTextView
class,
provides the functionality that users expect Cfrom a text editing
application. We’ve simply glued these features
together by adding just a few lines of
code.
[17] A bug in Project Builder (up to and including version 2.0.1) requires you to clean the project so that the new Info.plist settings can be incorporated into the application.