oreilly.comSafari Books Online.Conferences.


AddThis Social Bookmark Button

Giving the Simple Text Editor 'Legs'
Pages: 1, 2

Learning CocoaLearning Cocoa
By Apple Computer, Inc.
Table of Contents
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


- (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 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.