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


Programming With Cocoa Adding Spit and Polish to Your Cocoa App

by Michael Beam
06/14/2002

Many classes in Cocoa take delegates that provide guidance to a variety behaviors in a class. NSApplication is one such Cocoa class that may take a delegate -- one that provides many features we have come to expect from any application, but may have been unaware as to how to implement them in Cocoa.

We'll take a close look at two of NSApplication's delegate methods today that allow us to specify whether or not the application should present the user with an untitled document when it launches, and that provides a means for the application to open documents whose icons have been dropped on the application icon.

Additionally, as part of a greater effort to add some polish to ImageApp, we will learn how we can control the document window's title to display arbitrary information. Finally we'll endow ImageApp with a customized about panel.

The Application Delegate

The application delegate controls the behaviors described above by implementing two methods in particular: -application:openFile:, and -applicationShouldOpenUntitledFile:. The first of these tells the application how to open files dropped on the application icon; the second tells the application whether it should open an initial untitled document.

The latter is especially important in an application such as ours where the goal is not content creation, but rather content viewing. In other words, an empty canvas would be superfluous without any way to paint on it.

The first thing you'll probably want to know is how to assign an object as the delegate to our NSApplication instance. Not surprisingly, assigning an NSApplication's delegate is done most easily in Interface Builder (IB). Out of the two nibs we have, MainMenu.nib is the only one that has any relation whatsoever to NSApplication, as NSApplication is the nib's owner (i.e. the class of File's Owner in the nib is NSApplication). We'll work with this nib.

Building Cocoa Applications: A Step by Step Guide

Related Reading

Building Cocoa Applications: A Step by Step Guide
By Simson Garfinkel, Michael Mahoney

Open up MainMenu.nib in Interface Builder, and under the Classes tab create a subclass of NSObject. Name this subclass AppController, and instantiate it. In the Instances tab connect AppController to the delegate outlet of File's Owner. With that we've designated AppController as the official ImageApp application delegate. Not bad.

Now double-click on AppController to take you back to the Classes tab, and create the files for AppController so we can work with them in Project Builder. Save your work and return to Project Builder.

In general, implementing -applicationShouldOpenUntitledFile: can be as simple as returning YES or NO in response to the question being asked in the method name, or it can be as complicated as looking up a user default and returning a BOOL based on the stored default. More complicated schemes are possible, but likely don't have any practical purpose. By default document-based applications do open new untitled documents, so to disable this behavior we do the following:

- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)theApplication
{
    return NO;
}

The second delegate method we are going to implement is -application:openFile:. This method will enable the application to open a file dropped on either its Finder or Dock icons, provided we implement it correctly. So how do we programmatically control the creation of documents from paths and such? Why, with NSDocumentController, of course!

Two columns ago we learned that NSDocumentController plays a critical role in Cocoa's multiple-document system as a document factory. That is, NSDocumentController knows how to respond to actions sent by the Open and New File menu items. We can, of course, invoke file creation and opening methods in NSDocumentController ourselves, which is the route we'll take to implement -application:openFile:.

Everything we need to load the contents of a file into a document is provided with the file path in -application:openFile:. Let's take a look and see how we accomplish this:

- (BOOL)application:(NSApplication *)theApplication
           openFile:(NSString *)filename
{
    NSDocumentController *dc;
    id doc;

    dc = [NSDocumentController sharedDocumentController];
    doc = [dc openDocumentWithContentsOfFile:filename display:YES];

    return ( doc != nil);
}

Every document-based application has a document controller, which we retrieve using the class method +sharedDocumentController. Next we send an -openDocumentWithContentsOfFile:display: message to the document controller and assign the variable doc to the returned document object.

This method takes the path of the document, provided by the openFile: parameter of the delegate method, creates a new instance of MyDocument, and tells that object to load the contents of the file as the document's data. If the document controller was not able to load the file, then nil is returned. We can use the value of doc to test whether the operation was successful

The display: argument tells the document object whether it should create the interface to the document -- e.g. the window. If you're wondering why you wouldn't want to show the interface to a document, consider situations when ImageApp is being operated by something other than a user, say by AppleScript. AppleScript certainly doesn't need to view the contents of the document like human users do. All it needs to be able to do is create an instance of the NSDocument subclass for the document, and operate on it.

This leads us into a whole area of application design to support scriptability, which we will delve into further another time. One of the basic tenets of making scriptable applications is that the ability to operate on a document's data should not depend on the presence of the document's user interface -- the document should not need to display its contents to manipulate them. You should keep this in mind when you build your document classes; it will make life easier down the road should you opt to make your application scriptable.

So that was a long answer to the question of why display: would ever be NO. To close out the implementation of this method we check to see if an object is assigned to doc, and return a BOOL indicating the success of the open operation. Voila! Compile it, give it a run, and see how it works for you.

NSApplication declares many other interesting delegate methods you may find useful for your application. We won't get into all of the details, but let me give you an overview of them. The method -applicationDockMenu: returns an NSMenu object that will be displayed as the application's dock menu. This is useful if you want your application to support a dynamic dock menu that changes in response to a changed state of the application. The returned menu can be created programmatically, or it can be loaded from a nib.

Another delegate method some of you may find useful and interesting is -applicationShouldTerminateAfterLastWindowClosed:. The default behavior of Cocoa applications is to stay open even after the last window has closed. However, if you were to build a utility, you could follow the example of the majority of Apple's utilities and have this method return YES.

Thus, when the last window is closed, the application will quit. Now, don't abuse this method. Mac users don't expect their applications to quit when the last window is closed, and would probably be unhappy if random apps did work like this. This is especially true for document-based apps, but not as true for applications that users might run briefly to observe some state of the operating system, as many of the utilities do.

There are also several delegate methods that are sent in response to the application becoming the active application, or resigning this status; there are several sent in response to the application hiding and unhiding; delegate methods are even invoked in response to the application launching and terminating.

The methods related to application-launching could be useful if you wanted to implement a splash screen for your application at launch time. For example, the method -applicationDidFinishLaunching: could be used to close a window serving as a splash screen.

Many of these delegate methods also have accompanying notifications posted to the default notification center when the method is invoked. For example, if you wanted to execute some code in response to the application becoming the active application, you could implement in the application delegate the method -applicationDidBecomeActive:, or you could register an unrelated object in your application to respond to the notification NSApplicationDidBecomeActiveNotification.

The advantage of notifications is that you don't have to explicitly declare an object as a delegate to the application; you only have to register the object to respond to whatever notification you're interested in.

So that's the story with NSApplication's delegate methods. Let's move on now to see how to customize the document window's title.

Changing the Document Window Title

Ordinarily document windows display as their title the last component of the path under which the document is saved -- that is, the document name. Thus, a document saved at /Users/mike/Pictures/pigs.jpg displays the window title "pigs.jpg."

It's possible, however, to customize your document window's title to display information in addition to (or in place of) the document name. We see this sort of thing in Photoshop where the current zoom factor and the color space of the image are appended to the document name in the window title.

By overriding a method of NSWindowController, it's not difficult to put whatever we want in the window title. The method to override is -windowTitleForDocumentDisplayName:, which returns a string to display as the window title, and whose sole argument is the document name.

As an example of what we can do, let's implement this method to append the current scale to the document name in the following way:

- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName
{
    return [displayName stringByAppendingFormat:@" @ %2.2f%%", scale * 100];
}

If you don't understand the formatting used in the string above, you might want to read up on the printf function (using your favorite C book, or by typing man printf in the Terminal). Essentially, we're saying the number should be displayed as a float with two digits to the right of the decimal place, followed by a percent sign. (Since "%" is used to escape formatting we have to precede a "%" we want to appear as a character in the string with that same formatting escape character).

The window title produced by this method looks like the following:

Screen shot.
The window title produced by the above method.

Now there's an issue with this method. Whenever we change the scale of the image, we'd like the window title to reflect those changes. We need to tell the window controller the state of the window contents has changed, and that change in state needs to be reflected in the information displayed as part of the window title. NSWindowController provides a method to force an update to the window title, -synchronizeWindowTitleWithDocumentName. Since the zoom factor is displayed in the title, we want to update it whenever the zoom changes. The -scaleImageTo: is the lowest common denominator for all zoom operations, so we put as the last line in that method:

[self synchronizeWindowTitleWithDocumentName]

With that we've added another bit of polish to our application. To finish things up today we're going to add a custom About panel to our application.

A Custom About Panel

When making our About panel, we'll use a nib dedicated to this sole purpose. The main reason you might want to put the About window in a separate nib is to improve the launch time of your application, as well as to decrease the amount of memory it requires. When the application starts, it has to load the entire contents of MainMenu.nib into memory, so by putting the About window in a separate nib, the application has one less object to load with MainMenu.nib at launch time. The nib with the About panel isn't actually loaded until the user needs to see it.

Granted, for our relatively small application and small About panel, a separate nib won't yield significant improvement. However, the technique is worth learning and practicing for when your applications get larger and more complex.

In Interface Builder create a new nib from File-->New...; in the Starting Point dialog choose Empty from the list of templates. As the name suggests, this will create an empty nib file, to which you will now add a window. Go ahead and add any text or images to the About panel to reflect the information you would like displayed. There shouldn't be any controls like buttons or sliders for our implementation.

Save the nib as AboutPanel.nib, and put it in the English.lproj folder of your ImageApp project folder. You will then be prompted to choose what target to add the nib to. Choose ImageApp. I kept mine simple and uninformative, as shown in the image below:

Screen shot.
Probably not the best About panel an application could have.

To open the window in response to the user clicking on the "About ImageApp" menu item, we need a controller class for the About panel nib that has an outlet through which we can communicate with the About window. That is, so we can send the window a message telling it to display itself onscreen.

As you can see, AboutPanel.nib presently has no object capable of serving this purpose. To fulfill this role we employ the services of our class AppController. With a bit of modification it can be set up to serve the dual purpose of application delegate, and controller for the About panel. To prepare AppController for its new job, add to the interface file the outlet declaration:

IBOutlet NSWindow *aboutWindow;

Save the changes to this file, and drag it into AboutPanel.nib to add AppController to the nib's repertoire of classes.

Now we're not going to create an instance of AppController in the About panel nib like we have in MainMenu.nib -- our application only needs one instance of this class. Rather, we will make the class of AboutPanel.nib's File's Owner AppController. This brings me to an aside about File's Owner.

An important thing to remember about File's Owner is that it is not an instance of the class we assigned to File's Owner. The nature of File's Owner is quite different from that of the window object, or the instance of AppController in MainMenu.nib. The latter are actual instances of classes saved to disk, and then reloaded to memory when the nib is loaded.

Such is the wonder of Interface Builder, Cocoa, and nib files. You'll see this commonly referred to as an example of Cocoa's ability to "freeze-dry" objects (which is accomplished using the classes NSArchiver and NSUnarchiver. We'll cover these down the road.).

File's Owner, on the other hand, is a proxy object: a stand-in object that represents a link to an instance of its designated class that exists elsewhere at runtime. The reason we assign a class to File's Owner is so we can make meaningful connections that will become true connections between objects when the nib is loaded at runtime. These connections don't exist yet in the nib, as connections between real instances do. We will see in a moment how we tell the nib at runtime what object is really the File's Owner.

Before we get to that, make a connection the window and the About panel outlet of File's Owner. Save your work in AboutPanel.nib, and let's return to Project Builder to make a method to load the nib and open the window.

The next step is to make a point of access to it. That is, we need to write a method that will load the nib containing the window, and open the window in response to the user selecting the "About ImageApp" menu item. This method will go in AppController, which is convenient since the instance of AppController is part of MainMenu.nib where we can easily connect this action to the menu item in question.

To set this up add to AppController.h this method declaration:

- (IBAction)showAboutPanel:(id)sender;

Open MainMenu.nib in Interface Builder and read in AppController.h to update Interface Builder's information about the available methods of AppController. Next, select "About NewApplication" from the NewApplication menu, and change its name to "About ImageApp" (change any other NewApplication to ImageApp as well). Next open the Connections Info Panel and disconnect its current connection to File's Owner. The default connection is to an action of NSApplication that opens the default Cocoa About Panel.

Now drag a new connection from the menu item to AppController, and make the connection to the showAboutPanel: action. Save the changes to MainMenu.nib, and head on back to Project Builder.

Now we will see the code manifestation of our lengthy discussion of File's Owner. Remember, I said that File's Owner isn't a real object until we load the nib and assign an object as the owner. To do this we use a method of NSBundle called -loadNibNamed:owner:. This should set off alarms in your head; the second argument is owner:.

The object we pass as this argument is the object to which all of our fake connections to File's Owner will become real connections. The only restriction to the object passed here is that it must be of the same class that we assigned to File's Owner in the nib. For our purposes, we will pass self, which is the instance of AppController from MainMenu.nib. The first argument is the name of the nib that we wish to load, in our case AboutPanel.

Let's take a look at the implementation of -showAboutPanel:

- (IBAction)showAboutPanel:(id)sender
{
    if ( !aboutWindow )
	[NSBundle loadNibNamed:@"AboutPanel" owner:self];
    
    [aboutWindow makeKeyAndOrderFront:nil];
}

You see here that the nib-loading method is encased in an if statement. All this says is that we only want to load the nib if aboutWindow doesn't point to an object. Before we load the nib, aboutWindow is nil. When we load the nib with self as the owner, the connection between the window and the aboutWindow outlet is actualized, and aboutWindow points to a real object.

Subsequent calls to -showAboutPanel: will pass over the nib-loading code since it is already loaded in memory. Finally, we display the window by sending a makeKeyandOrderFront: message to aboutWindow. And that's all there is to it!

When we started this section I said not to put any controls in the About panel. That was for simplicity's sake. Adding controls is easy. All you have to do is declare the actions you want the controls to invoke in AppController, and make the connections through File's Owner.

If you have a lot of controls, then you might be better off creating a dedicated AboutPanelController class. If you choose to do so, follow the example of using AppController as the About panel controller. That is, create and instantiate the controller class in MainMenu.nib so you can connect the About ... menu item to it, and then make it the File's Owner of the AboutPanel.nib.

The End

So there you have several things you can do to add polish to your application before releasing it into the wild. I hope you got plenty of ideas to take off on your own with whatever application you're building. In the next column we'll get into the class NSBitmapImageRep, and we'll learn how to work with images on a pixel-by-pixel basis by building a class that will convert color images into grayscale images.

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.


Copyright © 2009 O'Reilly Media, Inc.