Now that we’ve covered the Foundation, we’re going to take a step up and start working with the AppKit framework to create GUI-based applications. In this chapter, we’ll build a single-window application from beginning to end, letting us introduce the various GUI subjects necessary to become proficient with Cocoa programming. For the first time, you’ll see the complete workflow typical of Cocoa application development, composed of the following steps:
Design the application.
Create the project using Project Builder.
Create the interface using Interface Builder.
Define the classes using Interface Builder.
Connect the Model, View, and Controller objects using Interface Builder.
Implement the classes using Project Builder.
Build and run the project using Project Builder.
The application we’ll build in this chapter is a currency converter—a simple utility that converts a dollar amount to an amount in some other currency. This example has been one of the mainstay examples of NeXTSTEP/OpenStep/Cocoa programming; it’s been around almost long enough to reach “Hello World” status. Although it is a simple application, it consolidates quite a few of the concepts and techniques needed to get started with writing Cocoa GUI applications.
After working through this first complete GUI application, we’ll spend the rest of this section of the book exploring in-depth the topics introduced in this chapter.
Graphical user interfaces in Cocoa are built on the following four concepts:
Windows
Nib files
Outlets
Actions
A window in Cocoa looks similar to windows in other user environments, such as Microsoft Windows or earlier versions of the Mac OS. A window can be moved around the screen and stacked on top of other windows like pieces of paper. A typical Cocoa window, shown in Figure 5-1, has a titlebar, content area, and several control objects.
Many user-interface objects other than the standard windows are window objects without the standard window widgets. These include menus, pop-up menus, dialog boxes, sheets, alerts, panels, info windows, tool tips, tool palettes, and scrolling lists. In fact, anything drawn on the screen must appear in a window. End users, however, may not recognize or refer to them as “windows.”
A nib file is an archive of object instances generated by Interface Builder. Unlike the product of many user interface-building systems, a nib file is not generated code. It is a set of true objects that have been encoded specially and stored on disk. The objects in the nib file are created and manipulated using Interface Builder’s graphical tools.
Nib files typically package a group of related user-interface objects and supporting resources, along with information about how the objects are related—both to one another and to other objects in your application. Nib files hold all of the objects they describe by specially archiving, or freeze-drying, so that they can be reconstituted in a running application and then used again.
Every application with a graphical user interface has at least one nib file that is loaded automatically when the application is launched. The main nib file typically contains the application menu. Auxiliary nib files contain the application windows, as well as their associated user-interface objects. For example, an image-manipulation program such as Photoshop might have auxiliary nib files for the various palettes and controls that let you work with an image.
It can be useful to think of the objects that compose a user interface, and are contained within a nib file, as forming a hierarchy. Figure 5-2 shows the ownership hierarchy of nib-based objects for Figure 5-1. At the top of the nib file’s hierarchy of archived objects is the file’s owner object, a proxy object pointing to the actual object that owns, or controls, the nib file—typically the object that loaded the nib file from disk.
An
outlet
is a special-instance variable, marked with the
IBOutlet
keyword in a class’s
header, that contains a reference to another object, as shown in
Figure 5-3. An object can communicate with other
objects in an application by sending messages to them through
outlets.
An outlet can reference any object in an application: user-interface
objects, instances of custom classes, and even the application object
itself. What distinguishes outlets from other instance variables is
that Interface Builder recognizes the IBOutlet
keyword and lets you manipulate the connections it defines. These
connections, once defined, will be linked up for you when your
application runs. Specifying these relationships between objects in
Interface Builder saves you from having to write initialization code
by hand. There are ways other than outlets to reference objects in an
application, but outlets and Interface Builder’s
facility for initializing them are a great convenience.
Actions
are special methods, indicated with the IBAction
keyword, which are defined by a class and triggered by user-interface
objects. Interface Builder recognizes action declarations in a header
file, as it does with outlets. Similarly, Interface Builder allows
you to connect actions that a user might take with an interface, such
as pushing a button, to methods on an object. These connections are
shown in Figure 5-4.
An action refers both to a message sent to an object when the user clicks a button (or manipulates some other control), and to the invoked method.
Cocoa applications make use of a long-standing object-oriented paradigm called Model-View-Controller (MVC). As illustrated in Figure 5-5, MVC proposes three types of objects in an application—model, view, and controller:
The MVC paradigm works well for many applications, because the controller’s central and mediating role frees the model objects from needing to know about the state and events of the user interface. Likewise, the view objects don’t have to know about the programmatic interfaces of the model objects. Dividing the problem along these lines helps encapsulate the various objects in an application. This can also aid reuse, since the model could be used elsewhere, perhaps even on another platform.
MVC, strictly observed, is not advisable in all circumstances. Sometimes it can be advantageous to combine roles. For example, in an graphics-intensive application, such as an arcade game, you might have several view objects that merge the roles of view and model for performance reasons. In other applications, especially simple ones, you can combine the roles of controller and model; these objects join the special data structures and logic of model objects with the controller’s hooks to the interface.
The Currency Converter application
will consist of two custom objects: a Converter
that will serve as our model and a Controller
that
will mediate between the user interface and the
Converter
object. We’ll create
the view of the application using a collection of AppKit objects,
which we’ll assemble using Interface Builder. The
relationships between these objects are shown in Figure 5-6.
The Controller
object will assume the central role
in the application. Like all controller objects, it communicates with
the interface and model objects, and it handles tasks specific to the
application. The Controller
object gets the values
that users enter into fields, passes these values to the
Converter
object, gets the result back from the
Converter
, and puts this result into a field in
the interface. By insulating the Converter
from
the implementation-specific details of the user interface, the
Converter
object becomes a reusable component for
other applications.
Now that we have designed the application, we can get to work on the implementation. In Project Builder, create a new Cocoa Application project (File → New Project → Application → Cocoa Application) as shown in Figure 5-7, then name the project “Currency Converter”, and save it in your ~/LearningCocoa folder.
If you click on the disclosure triangle next to Other Sources in the left pane and click on main.m, you’ll notice that the file looks a bit different from the Foundation projects we’ve worked with in the past. The main.m file contains the following code:
// // main.m // Currency Converter // // Created by James Duncan Davidson on Fri Aug 30 2002. // Copyright (c) 2002 __MyCompanyName__. All rights reserved. // #import <Cocoa/Cocoa.h> int main(int argc, const char *argv[]) { return NSApplicationMain(argc, argv); }
Notice that the import
statement has changed to
importing Cocoa.h instead of
Foundation.h. The Cocoa.h
header contains the definitions for both the Foundation
and AppKit classes. Also notice that the main method makes a call to
the NSApplicationMain
function. This function is
defined by the AppKit and will start the application, load the main
nib file, and set up the event loop and autorelease pool for that
loop. Now that we have taken a look at this source file, we can let
it be. You’ll very rarely, if ever, modify the
main.m file of a Cocoa GUI application.
Project Builder automatically generates the comments at the top of
the source file from the Cocoa Application template.
You’ll probably want to change the
__MyCompanyName__
text to the actual copyright
holder and make sure that the copyright year is correct. See Chapter
17 for more details on how to finish and polish your applications.
The Currency Converter’s interface is actually quite simple to create. It consists of a few text fields and a button. The process of creating it will give you an opportunity to explore how Interface Builder works. Figure 5-8 shows a hand-drawn sketch of how we’d like the interface to look. This gives us something to go by when designing the interface in Interface Builder.
Begin by creating an application’s user interface in Interface Builder.
In Project Builder’s left pane, click on the disclosure triangle next to Resources to reveal the MainMenu.nib file.
Double-click on MainMenu.nib to open it in Interface Builder.
A default menu bar and window, titled Window, will appear when the nib file is opened.
The window is a bit large for our purposes. You can change the size either by dragging the bottom-right corner of the window or by using the Info window, as shown in Figure 5-9. You can open this window by selecting Tools → Show Info from Interface Builder’s menu (Shift-
-I).
When you have opened the Info window, use the following process to resize the window:
Select Size from the Info window’s pop-up menu.
In the Content Rect area, select Width/Height from the right-hand pop-up menu.
In the text fields under the Width/Height menu, type
400
in the width (w) field
and 200
in the height (h)
field, as shown in Figure 5-9.
By default, our window has a title of Window. We want the application window to have a more meaningful title, as well as a few other attributes that we care about.
Select Attributes from the Info window’s pop-up menu, and change the window’s title to Currency Converter, as shown in Figure 5-10.
Verify that the Visible at launch time option is selected. This will ensure that this window is created on screen when the application is launched.
Deselect the Resize checkbox in the Controls area. This will prevent users from resizing the application.
The Currency Converter will use text fields to accept user input and display converted values. To place a text field into the window:
Drag an NSTextField
object from the Views palette
(shown in Figure 5-11), and place it in the
upper-right corner of the application window.
When you drag the text field onto the window, Interface Builder helps you place objects according to the Aqua Human Interface Guidelines (HIG) by displaying guidelines when an object is dragged close to the proper distance from neighboring objects or the edge of the window.
Resize the text field by grabbing a handle and dragging it in the direction in which you want it to grow. In this case, drag the left handle to the left to enlarge the text field, as shown in Figure 5-12.
Just as you can specify the size of the application window, you can
also specify exact sizes for other elements of your application. For
example, if you want the text field to be 150 pixels wide, select the
NSTextField
object, and then select Size from the
NSTextField Info window (Shift-
-I). In
the width field (w), enter 150
as the value, and
hit the Tab key to accept the value; the
NSTextField
object will conform to its newly
defined dimensions.
Currency Converter needs two more text fields, each the same size as the first. To place these fields, you have two options: you can drag another text field from the palette and make it the same size, or you can duplicate the first object. To create a new text field by duplication:
Select the text field, if it is not already selected.
Choose Edit → Duplicate (or use the keyboard shortcut,
-D). The new text field appears slightly offset from the original field.
Another way to duplicate a field is to click on the object, then hold down the Option and drag the object. A plus sign will appear next to the pointer to indicate that you’re making a copy of the object, and the guidelines will help you move the newly duplicated object into place.
Reposition the new text field under the first text field. You’ll notice that the guides will appear once again to help you move the second text field into place.
To make the third text field, make another duplicate. Notice that Interface Builder remembers the offset from the previous Duplicate command and automatically uses that offset to create and place the third text field.
Since the third text field will display the results of the computation, it should not be editable. To change its attributes:
Select the third text field.
Choose Attributes from the Info window’s pop-up menu, as shown in Figure 5-13.
In the Options section of the Info window, uncheck the Editable attribute so users cannot alter the contents of the field.
Make sure that the Selectable attribute is on so that users can copy and paste the contents of this field to other applications.
Next we need to add some labels to the text fields, so the user will know why the fields are there.
Using Figure 5-14 as a guide, drag a System Font Text object from the Views palette onto the Currency Converter window.
Right-align the text using the Info window.
Duplicate the text label twice, and then edit the text for all three labels, as shown in Figure 5-15. To edit a text label, double-click on the current label (System Font Text) to highlight it, then type in the new label. After entering the new label, hit the Tab key to accept the new label.
As you type in the new labels, you’ll notice that the text fields aren’t wide enough to hold the text shown in Figure 5-15. To correct this problem, resize the text fields by grabbing the middle-left field holder and dragging the edge of the text field to the left until all of the text appears.
The last functional part of the user interface that we need to add is the Convert button. It needs to be set up so that it can be invoked either by clicking it or by pressing Return when the application has the user’s focus.
Drag a button object from the Views palette, and place it in the lower-right portion of the window.
Double-click the title of the button to select its label, and change the title to Convert.
With the button selected, choose Attributes in the Info window, and then choose Return from the pop-up menu labeled Equiv, as shown in Figure 5-16. This allows the button to respond to the Return key, as well as to mouse clicks.
Align the button under the text fields. To center the button under the text fields, you can pop up a set of measurement guides that tell you the distance from an object to any of its neighboring objects. With the button selected, hold down the Option key and point to an object whose distance from the button you want to see, as shown in Figure 5-17. With the Option key still down, use the arrow keys to nudge the button to the exact center of the text fields.
Lines can separate elements to help the user make sense of the objects in the user interface. We’ll add a line between the editable fields and the result field.
Move the third text field and label down a bit in the user interface.
Drag a horizontal line from the Views palette onto the interface, and use the alignment guides to place it right under the dollars text field.
Use the selection handles on the line to extend it to each side of the interface.
Move the result text field and label back up into position using the guides, then move the Convert button up into place.
Resize the window using the guides to give you the proper distance from the text fields on the right and the Convert button on the bottom.
At this point, Currency Converter’s application window should look like Figure 5-18.
The final step in composing Currency Converter’s interface has little to do with appearance and everything to do with behavior. When users launch the application, they should immediately be able to enter information in the interface and tab between the text fields.
The first place a user’s input should go when they launch the application is in the first text field. To ensure that this happens, specify the first text field as the application window’s initial first responder -- the object in the window that will be first in line to accept events from the keyboard. To do this:
In the Instances pane of nib file window, click on the Window instance and Control-drag a connection to the first text field in Currency Converter’s window.
Select the initialFirstResponder
outlet in the
Info window, as shown in Figure 5-19, and click the
Connect button.
Next, we want to ensure that when the user presses the Tab key, the focus moves to another text field. To do this:
Select the first text field, and Control-drag a connection line from it to the second text field, as shown in Figure 5-20.
Select the
nextKeyView
outlet in the Info window, and click the
Connect button.
Repeat the previous two steps, but connect the second field to the first. This will make it so you can tab from the second field back up to the first text field.
The Currency Converter interface is now complete. Interface Builder lets you test the interface without having to write any code.
Choose File → Save All to save your work.
Choose File → Test Interface (
-R) to launch the interface in a mode where you can test it.
Try various operations in the interface, such as tabbing, cutting, and pasting between text fields.
When finished, choose Quit New Application (
-Q) from the Interface Builder application menu to exit the text mode.
Notice that the screen position of the Currency Converter window in Interface Builder is used as the initial position for the window when the application is launched. Place the window near the top-left corner of the screen so that it will be a convenient (and traditional) initial location.
We’ll define the two classes needed for our application here in Interface Builder: a controller class and a model class. If you recall, the controller class controls the interaction between the model and view objects, while the model object holds data and defines the logic that manipulates that data.
The controller class,
Controller
,
doesn’t need to inherit any special functionality
from other classes, so it will be a subclass of
NSObject
. To define it:
Click the Classes tab of the MainMenu.nib window, as shown in Figure 5-21.
Select NSObject from the list of classes.
Press Return to create a new subclass of NSObject
,
and rename it Controller
.
The Controller
object needs access to the text
fields of the interface, so you must create
outlets
for them.
Controller
will also need to communicate with the
Converter
class (yet to be defined) and thus
requires a fourth outlet for that purpose.
Select the Controller
class in the Classes window,
as shown in Figure 5-21.
Select the Attributes menu item in the Info window.
Add an outlet named rateField
by clicking the Add
button, entering the name, and pressing Return.
Create three more outlets, named dollarField
,
totalField
, and converter
, as
detailed in step 3.
The Controller
class needs only one
action method to respond to user-interface
events. When the user clicks the Convert button (or uses the Return
key, which we defined as an equivalent), we want a
convert:
message sent to an instance of the
Controller
.
Click on the Action tab in the Info window.
Add an action named
"convert:"
.
Interface Builder will add the
":"
for you if
you don’t.
Like the Controller
class, the
Converter
class—our
model in MVC speak—doesn’t need to inherit any
special functionality, so you can make it a subclass of
NSObject
. Because instances of this class
won’t communicate directly with the interface,
there’s no need for outlets or actions.
The last task that remains in Interface Builder is to hook up the various parts of our application so that each part can talk to the others.
When the application is first launched and the nib file is loaded, we want to create an instance of both our controller and model classes. To do this:
Select Controller
in the Classes tab of the nib
file window.
Choose Instantiate from the Classes menu. The instance will appear in the Instances view of the MainMenu.nib window, as shown in Figure 5-22.
Repeat the process for the Converter
class.
Now you can connect the
Controller
instance object
to the user interface. By connecting it to specific objects in the
interface, you initialize its outlets. Controller
will use these outlets to get and set values in the interface.
In the Instances display of the nib file window, Control-drag a
connection line from the Controller
instance to
the first text field, as shown in Figure 5-23.
Interface Builder will bring up the Connections display of the Info
window. Select the action that corresponds to the first field,
rateField
.
Click the Connect button.
Following the same steps, connect the
Controller
’s
dollarField
and totalField
outlets to the appropriate text fields.
To tell the controller that it is time to perform an action, we need
to hook up the Convert button to the
Controller
.
Control-drag a connection from the Convert button to the
Controller
instance in the nib file window.
Instead of dragging from the controller object to an interface
object, we are dragging a connection from a user-interface object to
the controller.
In the Connections Info window, make sure that the target is selected in the Outlets column, as shown in Figure 5-24.
Select convert: in the Actions column.
Click the Connect button.
Now we come to the part of this exercise where we take all of that work done in Interface Builder, generate the source files for our classes, and finish the class implementations in Project Builder.
To generate the source files, follow these steps:
Go to the Classes display of the nib file window.
Select the Controller
class.
Choose Create Files from the Classes menu.
Verify that the checkboxes in the Create column next to the .h and .m files are selected.
Click the Choose button.
Repeat Steps 1-5 for the
Converter
class.
Save the nib file.
You can also create the files for a class by Control-clicking (or right-clicking if you have a two-button mouse) on the class name in the Classes menu and selecting the “Create files for...” menu item.
Now, we leave Interface Builder for this application. You’ll complete the application using Project Builder.
When Interface Builder adds the header and source files to the Currency Converter project, it tries to put them in the same group folder as other source files in the same disk folder. Since the newly created files are class implementations, move them to the Classes group if Interface Builder did not do so automatically.
Click Project Builder’s main window to activate it.
Select the Controller and Converter files in the Groups & Files list, and drag them into the Classes group, as shown in Figure 5-25.
Look at the Controller.h file that Interface
Builder generated. Notice that in addition to being declared of type
id
, our variables have an
IBOutlet
declaration. This is a macro that, in the
compiler, doesn’t evaluate anything. It is used as a
hint to Interface Builder’s parser, telling it that
the variable is an outlet. You will also notice that the
convert:
method has a return type of
IBAction
. This type is the same
as void
and also tells Interface Builder that the
method serves as an action that can be hooked up to user-interface
elements and other objects. These declarations allow you to add
outlets and actions in the code and enable Interface Builder to parse
them. We’ll see this in action in later chapters.
We need to add a method to the
Converter
class that the controller object can invoke to perform our currency
conversion.
Start by declaring the convertAmount:atRate:
method in Converter.h, as shown in Example 5-1. This method declaration states that
convertAmount:atRate:
takes two arguments of type
float
and returns a float
value.
Add the method implementation to the Converter.m file, as shown in Example 5-2. This method simply multiplies the two arguments and returns the result.
Update the “empty” implementation
of the convert:
method in
Controller.m that Interface Builder generated
for you, as shown in Example 5-3.
#import "Controller.h" #import "Converter.h" // a @implementation Controller - (IBAction)convert:(id)sender { float rate = [rateField floatValue]; // b float amt = [dollarField floatValue]; // c float total = [converter convertAmount:amt atRate:rate]; // d [totalField setFloatValue:total]; // e } @end
The lines we added do the following things:
Imports the Converter
class interface.
Gets the value of the rateField
outlet of the
interface as a floating-point number. All text fields (and other
classes that inherit from NSControl
) can present
the data that they contain in various forms, including doubles,
floats, Strings, and integers.
Gets the value of the dollarField
outlet of the
interface as a floating-point number.
Calls the convertAmount:atRate:
method of the
Converter
object instance.
Sets the value of the totalField
outlet of the
interface to the result obtained from the
Converter
object instance.
When you click the Build and Run button, the build process begins. When Project Builder finishes—and hopefully encounters no errors along the way—it displays Build succeeded on its status line and starts the application.
To exercise the application, enter some rates and dollar amounts, and click Convert. Of course, the more complex an application is, the more thoroughly you will need to test it. You might discover errors or shortcomings that necessitate a change in overall design, in the interface, in a custom class definition, or in the implementation of methods and functions.