MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Giving the Simple Text Editor 'Legs'

06/01/2001

In the last article, I discussed how to create an interface for a simple text editor, complete with spell-checking, font-styling, and much more. If you followed along, you were able to create a feature-rich application without any actual coding work. But now you need to add a little code to make it complete. Our text editor, being an application that works with documents, needs to behave as such. In Cocoa these are called document-based applications.

Primarily (at least for the context of this column) that means we should be able to save documents to disk and then open them at another time. Cocoa's document-based application sub-structure provides a programmatic paradigm and interface for access to printing, document opening and saving, undo services, and management of multiple simultaneous open documents. The class NSDocument encapsulates and abstracts this behavior making it straightforward to create document-based applications. So it looks like it's time to talk about the NSDocument class.

The reason for starting you out in Cocoa with a document-based application rather than the traditional "Hello World" type of thing was to make you realize how much power is available from the get-go. This application requires very little coding, yet allows you to do so much.

Additionally, since document-based applications are so ubiquitous, it makes sense to give you something you can use right away. An application that can store and open data in the form of a document is far more powerful and useful than a glorified area-of-a-circle calculator. I realize I'm throwing some fairly advanced stuff at you in this column, so don't worry if you don't understand it fully this time around. I will be getting back to all of this in more detail later on.

NSDocument and Cocoa document-based applications

If you've ever done anything at the keyboard, it's been working with documents. After all, this is what we do with computers. A computer isn't very useful if we can't save data and retrieve it at a later time. The applications that work with documents are, appropriately enough, called document-based applications. Because of the inherent utility and pervasive nature of these types of applications, it is worth our time to learn how Cocoa implements them.

In the most functional sense, a document is no more than a container for data -- a bucket for bytes if you will. In Cocoa, a document is a very intelligent bucket (a bucket with brains?) that knows how to do lots of useful things with its data. It can display data in a window, save it to a file, read saved data from a file, and even print it. The document and its interface define how users interact with the data in the bucket. This behavior is encapsulated in the Application Kit class NSDocument.

Comment on this articleWhat are your observations to this point about programming in Cocoa?
Post your comments

Also in Programming With Cocoa:

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Build an eDoc Reader for Your iPod, Part 3

Build an eDoc Reader for your iPod, Part 2

It's important to understand the relation between NSDocument, the application, the users, and data. Every document that is open in its application is represented by an instance of a subclass of NSDocument. This means that you can have multiple documents open at the same time, and all of these documents are managed by the application.

In turn, each document object controls one or more windows that display the data of the document and allow users to interact with it. Understand that when we talk about documents, we can either be talking about the data -- all your spreadsheets and text files on your disk -- or we can be talking about the instance of an NSDocument subclass that does the work behind the scenes. More often than not, I'll be talking about NSDocument.

I said earlier that we work with instances of a subclass of NSDocument rather than an instance of NSDocument itself. NSDocument is actually a special kind of class known as an abstract class. What this means is that NSDocument declares many methods that encapsulate the essential behaviors of a document, but it does not attempt to define how those methods work. The implementation of these abstract methods is deferred to the subclass.

NSDocument doesn't know anything about the format or internal structure of a document's data, because every application has its own unique way of working with data. Rather, it is the responsibility of the subclass to provide the code that interprets the application's unique data. NSDocument sets up and provides a set of services needed by all documents with its abstract methods, and you create a subclass that contains all the code needed to turn data on the disk into a picture of a mountain for your eyes, or music for your ears.

When you create a Cocoa document-based application in Project Builder, a subclass of NSDocument called MyDocument is already set up and ready for you to fill in. MyDocument contains a skeletal set of methods you must define for the application to work properly. The rest of this column will be concerned with implementing these methods to work with the text editor we started last time (if you haven't read the last article, it might by prudent to go do so now). Let's get to it!

Back to the simple text editor...

Wiring up the interface

What we need to do now is make it possible to access and control the elements of your interface through source code. Interface Builder does this by letting you create outlets and actions in objects that we can work with in Project Builder.

Outlets are instance variables in a class that identify, or point to, another object, usually some interface object (buttons and text fields and such).

An action is a method that is invoked by some object in the interface; for example, when you push a button, it could be set up to invoke a particular action. We connect outlets and actions to the appropriate interface objects literally by wiring them together.

For our simple text editor, we need just one outlet that identifies our instance of NSTextView. To set this up, click on the "Classes" tab in the MyDocument.nib window. You will see a list of Cocoa classes available for you to subclass. Some of them are grayed out, and others are black. The black names indicate classes that you can add outlets or actions to; gray indicates a class that cannot be modified by the user without making a subclass.

Screen shot.
To set up an outlet that identifies our instance of NSTextView, click on the "Classes" tab in the MyDocument.nib window.

Scroll through the list and find the class named MyDocument, which is the subclass of NSDocument given to us by Project Builder. To the right of the class name you see two icons. The one that looks like an electrical outlet represents outlets; the other one which is a target represents actions.

Creating and connecting an outlet

The number next to each one indicates the number of outlets and actions that have already been declared. If you click on either one of these, you will see the MyDocument class expanded such that the outlets and actions are displayed. Here we see that there is already an outlet called window; this outlet is connected to the main document window in which we put the text view. Hit the Enter key while "outlets" is highlighted to create a new outlet, and name it "textView". Pressing tab or clicking in any of the gray area outside of MyDocument will return you to the previous view. You have now created an outlet.

Now we need to connect the textView outlet to the actual the NSTextView object in our interface. Return to the tab pane Instances. This pane shows all the objects that will be loaded into memory at runtime when the application loads your nib file. While holding the control key, click on the File's Owner icon and hold it while you drag the mouse over to the NSTextView window. When you unclick, the Inspector window will open and a list of all possible connections will be displayed. You should see the textView connection we just created. Highlight that and click the Make Connection button at the bottom of the window.

Screenshot.
While holding the Ctrl key, click on the File's Owner icon and hold it while you drag the mouse over to the NSTextView window.


Screenshot.
You should see the textView connection we just created. Highlight that and click the Connect button at the bottom of the window.

It should be noted that actions are wired in the direction of interface object to File's Owner, opposite of the direction for outlets. To help remember which direction you wire actions and which direction you wire outlets, think back to the way messaging works between objects that we talked about in the second column. When a button is pressed, it sends a message to MyDocument telling it to perform a method. The message from the button is requesting an action to be performed. Outlets on the other hand, are the receivers of messages. If you need to do something to the text in a text view, you must send the textView outlet a message to do so.

Now that we have made all the connections we need, let's go back to Project Builder and give our application some "legs."

Learning CocoaLearning Cocoa
By Apple Computer, Inc.
Table of Contents
Index
Sample Chapter
Full Description
Read Online -- Safari

Back to project builder

I said before that we're going to add functionality to our application by having MyDocument override specific abstract methods of NSDocument. In Project Builder, open the group Classes, and you will find the class interface and implementation files for MyDocument (remember, files with a .m extension are the implementation, and files with a .h extension are the interface, or header files).

Before we can implement the necessary methods, we have to let our source code know about the textView outlet we created. Again, an outlet is nothing more than an instance variable that points to an Interface Builder object. Because it is an object identifier, it will have data type id. Because it is also an Interface Builder outlet, we must modify the type to reflect this. Thus our instance variable will be typed IBOutlet in addition to id.

Clicking on MyDocument.h opens the class interface file in the text editor. Double-clicking it will spawn an independent text-editing window if you prefer that. Between the braces that follow the line @interface MyDocument : NSDocument add the variable declaration id IBOutlet textView. You see here how the instance variable is given the same name as the outlet, and typed to both id and IBOutlet. The names must be the same for the compiler to recognize that this instance variable is an Interface Builder outlet. While we're here let's add another variable. NSData *fileData, which we will use later to work with the contents of our document's file.

Screen shot.
You can quickly switch between a source and interface file by clicking this icon.

Now we want to look at the implementation file and study what we get by default, and modify it to our needs. Open MyDocument.m in the same way you did the interface file. You can quickly switch between a source and interface file easily by clicking the icon shown in the picture to the right.

The MyDocument.m source template has the following two methods that will be of interest to us:

- (NSData *)dataRepresentationOfType:(NSString *)aType

and,

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType

They are called by the application when a document is to be saved and loaded respectively.

In implementing these methods, we are assuming that the data type of our document is Rich Text Format with Attachments, or RTFD. RTFD is exactly like RTF, except it allows images and files to be associated with and embedded in the document. RTFD files are a bundle, like applications in OS X are. That is, the file seen in the Finder with an RTFD extension is a folder that contains the text file, as well as any embedded files or images.

We need to tell Project Builder that our document format is RTFD; actually, we're not telling it anything about the format in terms of data structure, but rather what the file extension, or signature is. To do this go to the Targets tab and click on the SimpleTextEditor target. In that view, click on the Application Settings tab.

There are two places we need to indicate the file extension in the Application Settings pane. In the Signature field of Basic Information type in rtfd. Now scroll down to Document Types and select the "DocumentType" line in the table. Below the table you can edit the type information; type in rtfd in the Extensions and OS Types fields. Scroll down further and click on Change. That's done now, so let's move on.

NSData

NSData is a class in the Foundation Framework that encapsulates raw data in memory. In a sense, it is like the smart bucket we talked about before, except it acts differently than the document bucket. It's still pretty smart, but in a different way. NSData takes a chuck of data -- it doesn't care what it is, it just sees it as an array of bytes -- and makes an object out of it. Like any object, the data is wrapped up in the methods that define the way it behaves and the way we interact with it.

Thus, there are several methods that let us do things like save and load the data from disk, and extract sub-data from it. The nice thing about NSData is that it provides a consistent and standard way of working with data of different types. Because of this consistency, many Cocoa classes exchange information and data using NSData instances. NSDocument is no exception, nor is NSText, and because of this, it is very easy to get data from the disk and into the window.

Back to the program

Let me explain in detail how an application such as this works with data and documents. When you choose to save a file through the menu or Command-S, whatever your preference is, the application will bring up a Save File sheet. Here the user moves around through directories, gives the file a name, and, if necessary, indicates the preferred format for the file. When you press the "Save" button a message telling your document -- represented by an instance of the class MyDocument -- to look at your document and return an NSData object containing a data representation of the contents of your document.

Opening a file is similar. When you select a file to open in the Open sheet, the application converts the contents of that file into an NSData object, and passes that to the loadDataRepresentation: ofType: method for your document to deal with in its unique way. Let's go ahead and implement these methods for our application.

Saving data to a file

Here is the code for saving our document:

- (NSData *)dataRepresentationOfType:(NSString *)aType
{
NSRange range = NSMakeRange(0, [[textView textStorage] length]);
return [textView RTFDFromRange:range];
}

NSRange is a standard C data structure -- not an object-predefined as part of the Foundation Framework that is just two numbers that define a range in the format of starting location and length. We can make a range using the NSMakeRange() function -- note, not a method -- as shown above. Here we have a range that begins at element 0, and has a length equal to the total number of characters in the document. This length is obtained by returning the NSTextStorage object, which is where the text is actually stored, associating it with our text view, and then asking it how many characters are in it by invoking the "length" method.

We then need to convert the text in textView into an NSData object as required by the return data type of the dataRepresentationOfType: method. The method RTFDFromRange: declared in the NSTextView superclass, NSText, does just that. It takes a range as an argument. In our case, the range includes our entire document, and converts the text in that range into data and returns it as an NSData object. The object returned by the message [textView RTFDFromRange:range] is then just turned around and returned as the NSData object returned by the dataRepresentationOfType: method.

Opening data files

Now we should talk about loading data from a file: This is done in the method loadDataRepresentation: ofType:. Here is the code for our implementation of that method:

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
fileData = data;
return fileData != nil;
}

It's simple; it does nothing more than copies the data given to this method as an argument from the Open File sheet, and sets the NSData variable to identify the same object pointed to by data. The return value of this method is a Boolean indicating success or failure of retrieving the data. We do a logical test to see if fileData points to the nil object-nothing-or to something else; if it does point to data, then "Yes" is returned.

In the implementation file is another method called windowControllerDidLoadNib:. This is the method that is invoked after the nib file has been loaded and our application is aware of the NSTextView object in our interface. Now we can send a message to textView telling it to load the data contained in the "file," and it will receive the message. Before the nib file was loaded, our message to load the data would have been received by thin air. The implementation for this method looks like this:

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

if ( fileData != nil ) {
[textView replaceCharactersInRange:NSMakeRange(0, 0) withRTFD:fileData];
}
}

The first line simply tells the superclass of MyDocument to perform its method of the same name. While we are overriding and customizing the windowControllerDidLoadNib: method defined in NSDocument, the code in NSDocument is not lost, we access it by sending the message to "super."

The conditional statement is what we're interested in. Basically, we test again to make sure fileData doesn't point to thin air -- the nil object. If fileData does identify some data object, then we send a message to textView saying to replace the first character of the blank document with the text contained in the data object identified by fileData. This is the standard way of loading up a text view with data

.

Building and running the program

Go ahead and build and run the application. This is done by pressing Command-R, or clicking on the hammer icon (to build) and then the monitor icon (to run).

Play around with your application. Type text in, format it, do whatever. Save your document and then try re-opening it. Pretty cool? So that's it! You've made you're first fully functional application! Are you impressed with Cocoa yet? Come back next time when we will build a simple color meter application as a vehicle for further exploring Interface Builder and Project Builder.

Copyright © 2009 O'Reilly Media, Inc.