Search the Catalog
Learning Cocoa

Learning Cocoa

By Apple Computer, Inc.
May 2001
0-596-00160-6, Order Number: 1606
370 pages, $34.95

Chapter 10
Travel Advisor Tutorial

Contents:

Travel Advisor Design
Create the Travel Advisor Interface
Define the Classes of Travel Advisor
Implement the Classes of Travel Advisor

In this chapter you'll expand your repertoire of Cocoa programming techniques to:

The vehicle for exploring these topics is an application called Travel Advisor. Travel Advisor is a forms-based application used for entering, viewing, and deleting records on countries to which the user travels. This example application will give you another chance to work with design using Model-View-Controller (MVC) and also provides a chance to explore more deeply many of the Cocoa programming techniques to which you've been introduced in previous chapters.

Travel Advisor Design

Travel Advisor is much like Currency Converter in its basic design, but the application is considerably more complex. Figure 10-1 shows Travel Advisor's user interface. To use Travel Advisor, you enter a country name along with travel-related information associated with that country. When you click Add, the name of the country appears in the table of country records. After you've entered data on several countries, you can select a particular country from the table and the information on that country appears in the forms. You can save all of your travel information to disk before you quit the application, so no data is lost between sessions. The application also allows you to do temperature and currency conversions.

Figure 10.1. Travel Advisor's user interface

Travel Advisor is based on the Model-View-Controller paradigm. A Controller object (TAController) manages a user interface comprised of Application Kit objects, and the controller sends messages to the Model objects to get the results of various data manipulations. The high-level design is depicted in Figure 10-2.

Figure 10.2. Travel Advisor design overview

Because one of the computations required for Travel Advisor is the conversion of various currencies, the Converter object from the Currency Converter project can be reused without modification.

Model Objects

Travel Advisor must display a unique set of data, depending on the country the user selects. To make this possible, the data for each country is stored in a Country object. These objects encapsulate data on a country (in a sense, they're like records in a relational database). The application can manage potentially hundreds of these objects, tracking each without recourse to a "hardwired" connection.

Another Model object in the application is the instance of the Converter class. This instance does not hold any data, but does provide some specialized behavior.

View Objects

Travel Advisor's view objects are all off-the-palette Application Kit objects: text fields, checkboxes, buttons, and so on.

Controller Objects

The Controller object for the application is TAController. Like all Controller objects, TAController is responsible for mediating the flow of data between the user interface (the View part of the paradigm) and the Model objects that encapsulate that data--the Country objects. Based on user choices in the interface, TAController can find and display the requested Country object; it can also save changes made by users to the appropriate Country object.

What makes this possible is an NSDictionary object (called a dictionary from here on). A dictionary, as you learned in Chapter 6, "Essential CocoaParadigms", is a container that stores objects and permits their retrieval through key-value associations. The key is a unique identifier that can be used to "look up" the associated object. To get the object, you send a message to the dictionary using the key as an argument (objectForKey:). For example:

NSColor *aColor = [aDictionary objectForKey: @"BackgroundColor"];

In this example, aDictionary holds one or more NSColor objects, each associated with a string key. When the dictionary object receives the objectForKey: message, it looks for an object associated with the argument BackgroundColor.

In Travel Advisor (as shown later in this chapter) a Country object holds the name of a country as an instance variable; this country name also functions as the dictionary key. When you store a Country object in the dictionary, you also store the country name (in the form of an NSString) as the object's key. Later you retrieve the object by sending the dictionary the message objectForKey: with the country name as argument.

How TAController manages data

The TAController class plays a central role in the Travel Advisor application, as shown in Figure 10-3. As the application's Controller object, it transfers data from the Model objects (Country instances) to the fields of the interface and, when users enter or modify data, back to the correct Country object. The TAController must also coordinate the data displayed in the table view with the current object, and it must do the right thing when users select an item in the table view or click the Add or Delete button. All custom code specific to the user interface resides in TAController. The mechanics of this activity requires an array.

Figure 10.3. Data handling by TAController

Storing data source information

TAController also manages the data source for the table view that is used to display a list of available countries. TAController stores the keys to the dictionary containing all of the known Country objects in an array object (NSArray), sorted alphabetically. When the table view requests data for display, the TAController "feeds" it the sorted list of objects in the array.

Creation of Country objects

Another important point of design is the manner in which the Country objects are created. Instead of Interface Builder creating them, the TAController object creates Country objects in response to users clicking the Add button.

Delegation and notification

An essential aspect of design not evident from the Figure 10-3 are the roles delegation and notification play. You'll see how these design patterns are used as you construct the application.

Create the Travel Advisor Interface

In creating the interface of Travel Advisor, you'll be exercising the capabilities of Interface Builder much more than you did with Currency Converter. Here is an overview of the steps you'll take in this section to complete the Travel Advisor interface:

  1. Create the application project.

  2. Open the application's nib file.

  3. Customize the application's window.

  4. Add text fields, labels, and buttons to the window.

  5. Add a form object to the window.

  6. Group the user interface objects.

  7. Add a text view.

  8. Add and configure a table view.

  9. Add an image to the interface.

  10. Add a menu and menu items.

  11. Add formatters.

  12. Make connections for interfield tabbing and printing.

  13. Test the interface.

Get Started

You should be familiar with many of the objects on the Travel Advisor interface because you've encountered them them in Chapter 7, "CurrencyConverter Tutorial". Figure 10-4 points out the objects that are new to you in this tutorial.

Figure 10.4. New interface elements

Create the Application Project

Start by creating a new project for the application.

  1. Start Project Builder.

  2. Choose New Project from the File menu.

  3. In the New Project panel, select the Cocoa Application project type and click the Next button.

  4. Name the application Travel Advisor.

  5. If you wish, click Set to select a location to save the project in a specific location of your choice. To use the default location, go on to step 6.

  6. Click Finish.

Customize the Application Window

Rename and resize the application's main window.

  1. Open the Resources group in the project browser.

  2. Double-click MainMenu.nib. (This will launch Interface Builder if it is not already running.)

  3. Bring up Interface Builder's Info window (Command-Shift-I) if it's not already open.

  4. Change the window title to Travel Advisor.

  5. In the Controls area, turn off the Resize attribute.

  6. Click the Window Info's pop-up menu and select Size.

  7. In the Content Rect area of the Info window, verify that the left pop-up menu says Top/Left.

  8. Set the x field to 30 and the y field to 80. This sets the starting location of the Travel Advisor window when the application first launches. Note that moving the Travel Advisor window in Interface Builder will change these values.

  9. Also in the Content Rect area of the Info window, verify that the right pop-up menu says Width/Height.

  10. Set the width to 700 and the height to 520.

Add the Text Fields, Labels, and Buttonsto the Window

Complete the first phase of the application's user interface.

  1. Position, resize, and initialize the objects, as shown in Figure 10-5. Hint: liberal use of the Duplicate function (Command-D) will make this step go much more quickly.

    Figure 10.5. Initial layout of Travel Advisor's interface

  2. Use the Info window (Command-Shift-I) to change the attributes of the text fields labeled Local and Fahrenheit so they are not editable, as shown in Figure 10-6. These fields are used to display the results of computations, so there is no need for the user to be able to edit them.

    Figure 10.6. Text field Attributes Info window

More about buttons

If in Interface Builder you select the English Widely Spoken switch and bring up the Attributes Info window, you can see that the switch is simply a type of button. (See Figure 10-7.)

Figure 10.7. Checkbox Attributes Info window

Buttons are two-state control objects. They are either off or on, and this state can be set by the user or set programmatically using setState:. For certain types of buttons (especially standard buttons like Currency Converter's Convert button), when the state is switched, the button sends an action message to a target object. Toggle-type buttons, such as switches and radio buttons, visually reflect their state. Applications can learn of this state with the state message. You can make your own buttons, associating icons and titles with a button's off and on states and positioning titles and icons relative to one another.

Add a Form Object to the Interface

Add the form object that will hold logistical information about a country.

  1. Drag the form object from Interface Builder's Views palette as shown in Figure 10-8.

    Figure 10.8. Adding a form object

  2. Increase the size of the form's fields by dragging the resize handle sideways. Make the fields the same width as the Languages field in the Other section of the Travel Advisor's UI.

  3. Create two more form fields by Option-dragging the bottom-middle resize handle downward.

  4. Remove the spacing between the text fields by Command-dragging the bottom-middle resize handle upward.

  5. Rename the field labels as shown. (Tip: Use the Tab key to move between fields in the form.)

  6. Right-align the labels so that the form looks like Figure 10-9.

    Figure 10.9. Finalized form object

Group the Objects on the Interface

To make titled sections of the fields, forms, and buttons on the Travel Advisor interface, group selected objects. By grouping them, you put them in a box.

  1. Select the two Convert buttons along with the Dollars, Local, Celsius, and Fahrenheit labels and text fields. To select the objects as a group, drag a selection rectangle around them or Shift-click each object. (To make a selection rectangle, start dragging from an empty spot on the window.) When all the objects are selected, they should look like Figure 10-10.

    Figure 10.10. Selecting objects for grouping

  2. Choose Layout Group In Box (Command-G). The objects are now enclosed by a titled box.

  3. Double-click Title to select it.

  4. Rename Title as Conversions.

  5. Repeat for the next two groups, Logistics and Other, as shown in Figure 10-11.

    Figure 10.11. Grouped objects

Boxes are a useful way to organize and name sections of an interface. In Interface Builder you can move, copy, paste, and perform other operations with the box as a unit. For Travel Advisor, you don't need to change the default box attributes, but you can choose a different box style if you wish.

The box, an instance of NSBox, is the superview of all of its grouped objects. (A view, simply put, is any object visible in a window.) As was discussed in Chapter 8, "Event Handling", a superview encloses its subviews and is the next in line to respond to user actions if its subviews cannot handle them.

Add the Text View

The text view on the DataViews palette consists of a text object (an instance of NSTextView) enclosed within a scroll view (an instance of NSScrollView). This object allows users to enter, edit, format, and scroll through arbitrary-length text with minimal programmatic involvement on your part.

  1. Drag the text view from the DataViews palette and drop it on the lower-left corner of the window, as shown in Figure 10-12.

    Figure 10.12. Adding a text view object

  2. Resize the text view so it occupies the entire lower-left portion of the Travel Advisor window.

  3. Resize the Notes and Itinerary For label so that its right edge is aligned with the text view. This will leave some extra space to programmatically insert the name of the country currently being viewed.

You don't need to change any of the default attributes of the text view (but you might want to look at the attributes that you can set, if you're curious).

Add and Configure the Table View

As you discovered in Chapter 9, "Data Functionality", a table view is an object used to display and edit tabular data. Often that data consists of a set of related records, with rows for individual records and columns for the common fields (attributes) of those records. Table views are ideal for applications that have a database component.

In this section you will configure a table view to display the list of available countries:

  1. Drag the table view object from the Tabulation Views palette, (see Figure 10-13).

    Figure 10.13. Adding a table view object

  2. Resize the table view to fill the area between the Country field and Notes label.

  3. Set the title of the first column to Countries. Double-click the column header to insert the cursor. Type Countries, then click anywhere outside the column or press Return.

  4. Make the table contain only one column. First, delete the unneeded column by selecting it (click on the column header) and pressing Delete. Next, hold the cursor over the right edge of the Countries column, wait for the cursor to change to a pair of horizontally opposed arrows, and then click and drag the column edge so that it's flush with the right edge of the view. If you go too far to the right, a horizontal scrollbar will appear at the bottom of the view. If this happens, just slide the column edge back to the left and try again. When you are done, click outside the table view to deselect the column.

  5. Resize the Country text field so that its right edge is aligned with the table.

To configure the table view, you must set attributes of two component objects: the NSTableView object and the NSTableColumn object:

  1. Select the table view.

  2. Set the NSTableView attributes, as shown in Figure 10-14. Since this is a single-column view and country names are of limited length, you need only the vertical scroller in case there are more countries than can be shown at once. Whether to show the grid is a matter of personal preference, but turn off Resizing and Reordering. The user shouldn't be able to affect the contents of the table directly.

    Figure 10.14. Table view Attributes Info window

  3. Double-click the table's interior and select the Countries column label.

  4. Set the NSTableColumn attributes, as shown in Figure 10-15.

    Figure 10.15. Table column Attributes Info window

  5. In the Identifier field, type the name with which you want to identify the column programmatically. For Travel Advisor, make this name the same as the column title.

Add an Image to the Interface

When used tastefully, images can add a very nice visual touch to an interface. Sometimes buttons are the preferred objects for holding images--for instance, when you want a different image for the various button states. But when buttons are disabled, any image they display is dimmed. For decorative images, use image views (NSImageView) instead of buttons.

Tag Image File Format (TIFF) is the preferred image file format for use with Cocoa. You can use any of the formats supported by the NSImage class, including:

Consult the reference documentation for NSImage and NSImageRep for additional information. Note that Cocoa also accepts any of the image formats supported by QuickTime, but the use of QuickTime incurs extra memory and performance overhead and is not recommended for static images in the application's user interface.

To make managing image files easier, Interface Builder can reference and use images included in your Project Builder project:

  1. In Project Builder, choose New Group from the Project menu.

  2. Name the group Images and put it in the Resources group.

  3. Choose Add File from the Project menu and add Airplane.tiff included with the example files.

  4. In Interface Builder, drag an image view onto the window from the More Views palette, as shown in Figure 10-16.

    Figure 10.16. Adding an image view object

  5. Click on the Images tab in the MainMenu.nib window.

  6. Drag the proxy for the image onto the image view.

It's also possible to add images directly from Interface Builder. When you drop an image over a button or image view, Interface Builder adds the image file to the project (if the project is currently open in Project Builder) and includes a reference to it in the nib file.

Now configure the image's properties:

  1. In Interface Builder, bring up the Attributes Info window for the image view and set the attributes as shown in Figure 10-17.

    Figure 10.17. Image view Attributes Info window

  2. Make the image view (and the enclosed image) small enough to fit between the titlebar and the Logistics group.

  3. Add a "velocity" line behind the airplane. (Tip: Use a horizontal separator.)

Add a Menu and Menu Items

Travel Advisor's menu contains default submenus and commands. You need a submenu and menu commands that are not included in the default set and that are not found on the Menus palette. Use the Submenu and the Item cells to create customized menus and menu items, respectively.

  1. In Interface Builder, select the Menus palette. The palette window will look like Figure 10-18.

    Figure 10.18. Menus palette

  2. Drag the generic Submenu item and drop it between the Edit and Window submenus.

  3. Double-click Submenu to select the menu title; change the name to Records.

  4. Click the new Records menu to expose the Item command.

  5. Click the Item and duplicate it three times (making four altogether) using Command-D.

  6. Change the command names to Add, Delete, Next, and Previous. (Tip: Use the Tab key to move between entries in the menu.)

  7. Add Command-key equivalents to the right of the Next and Previous commands: Command-Option-N and Command-Option-P. To make the key assignment, double-click the area to the right of the menu command (a small square will appear), and then type the key equivalent you wish to assign. The Records menu should look like Figure 10-19.

  8. In the File menu, change Print... to Print Notes....

    Figure 10.19. Records menu items

Add Formatters

As you learned in Chapter 6, "Essential CocoaParadigms", formatters are objects that translate the values of certain objects to specific onscreen representations. In this section you'll add formatters to some of the text fields on the user interface to more appropriately display currency values.

  1. Select the DataViews palette in the Palette window.

  2. Drag a number-formatter object and drop it over the Rate field, as shown in Figure 10-20.

    Figure 10.20. Adding a number formatter

  3. Click on the Rate field to select it and bring up the Info window (Command-Shift-I), if it is not already visible.

  4. In the Formatter display of the Info window shown in Figure 10-21, specify a rate format by selecting the table view row with the 9999.99 format.

    Figure 10.21. Text field Formatter Info window

  5. Repeat for the Dollars and Local fields, but apply a suitable format.

Make Connections for InterfieldTabbing and Printing

You can now connect many of the objects on the Travel Advisor interface through outlets and actions defined by the Application Kit. As you'll remember from the previous tutorials, objects are connected in Interface Builder by Control-clicking on a source object and dragging a connecting line to the destination object.

Windows in Cocoa have an initialFirstResponder outlet for the object in the window that should be the initial focus of events. Text fields have a nextKeyView outlet that you connect so that users can tab from field to field. Forms also have a nextKeyView outlet for tabbing. (The fields within a form are already interconnected, so you don't need to connect them.)

  1. Make a connection from the window icon in the nib file window to the Country field.

  2. Select initialFirstResponder in the Connections display of the Info window and click Connect.

  3. In top-to-bottom sequence, connect the fields and the form through their nextKeyView outlets. Start by connecting the Country field to the Logistics form.

  4. When you reach the Languages field, connect it with the Country field, making a loop.

  5. Connect the editable text fields in the Conversions section in a similar loop.

The Application Kit also has preset actions to which you can connect your application. The NSTextView object in the scroll view can print its contents as can all objects that inherit from NSView. To take advantage of this capability, "hook up" the menu command with the NSTextView action method for printing:

  1. Click on the Print Notes menu command to select it. The Connection area of the NSMenuItem Info window shows a preexisting connection to FirstResponder.print. Disconnect the connection to FirstResponder.

  2. Connect the Print Notes menu command to the text object in the scroll view.

  3. Select the print: action method in the Connections display of the Info window.

  4. Click the Connect button.

Test the Interface

You're finished with the Travel Advisor interface. Save your work (Command-S) and test it by choosing Test Interface (Command-R) from Interface Builder's File menu. Try the following:

Define the Classes of Travel Advisor

Travel Advisor has three classes: Country, Converter, and TAController. Only TAController has outlets and actions. And, rather than defining the Converter class, you are simply going to add it to the project from the Currency Converter project and reuse it.

Specify the Country and TAController Classes

Subclass NSObject to create the Country and TAController classes.

  1. In Interface Builder, bring up the Classes display of the nib file window.

  2. Select NSObject as the superclass.

  3. Choose Subclass from the Classes menu.

  4. Type Country in place of MyObject.

  5. Repeat for TAController.

Specify TAController's Outlets and Actions

Now that the interface has been laid out, you can define the outlets that let TAController communicate with the UI objects. While here, you can also add the outlet for the Converter object that will be reused from the Currency Converter project.

  1. Select TAController and choose Add Outlet from the Classes menu.

  2. Add the following outlets:

    celsiusField

    commentsField

    commentsLabel

    converter

    countryField

    countryTableView

    currencyDollarsField

    currencyLocalField

    currencyNameField

    currencyRateField

    englishSpokenSwitch

    fahrenheitField

    languagesField

    logisticsForm

In addition to outlets, you must specify actions so that the UI objects can message TAController.

  1. Select TAController and choose Add Action from the Classes menu:

  2. Define the following action methods:

    addRecord:

    blankFields:

    convertCurrency:

    convertTemp:

    deleteRecord:

    handleTVClick:

    nextRecord:

    prevRecord:

    switchClicked:

Reuse the Converter Class

In Cocoa there are many ways to reuse objects. For example, subclassing an existing class to obtain slightly different behavior is one way to reuse the functionality of the superclass. Another way is to integrate an existing class--like the Converter class--into your project.

  1. In Project Builder, select the Classes group in the project browser.

  2. Choose Add Files from the Project menu and navigate to the Currency Converter project directory. Select both Converter.m and Converter.h. Click Open.

  3. When asked if you want to add the file to the Travel Advisor target, make sure the Copy checkbox is checked and the Travel Advisor target selected before you click Add.

  4. Open Travel Advisor's MainMenu.nib file.

  5. Drag the Converter.h file from Project Builder to the MainMenu.nib window in Interface Builder. Interface Builder parses the header file, looking for the superclass and all declared outlets and actions. The Classes panel of the nib file will now list Converter as a subclass of NSObject.

When you're finished with this procedure, the Converter class is copied both to the Travel Advisor project and to the Travel Advisor main nib file.

Generate TAController and Converter Instances

You don't need to instantiate the Country class in the nib file because it is not involved in any outlet or action connections. However, you must create an instance of TAController for making connections to other objects. TAController interacts behind the scenes with users as they manipulate the application's interface and mediates the data coming from and going to Country objects. It therefore needs access to interface objects and should be made the target of action messages. It also needs to connect to a Converter object, so instantiate Converter, too.

Make Connections to the TAController Instance

The TAController outlets need to be connected to the interface objects and the Converter object so that they can communicate with these objects at runtime. Remember to make the connections (Control-click and drag) in the direction that messages will flow. For example, TAController will send messages to the Celsius text field to draw text into it. So to connect these objects you would Control-click on the TAController instance and drag the connection to the Celsius text field.

  1. Connect TAController to the outlets listed in the following table. After making the first few connections in the list, it may seem as if Interface Builder is reading your mind because the proper outlet will already be selected in the Connections Info window each time you make a new connection. This is simply an artifact of the fact that the list in the table happens to be in alphabetical order. This makes things go more quickly for the purposes of the tutorial. If you connect the TAController instance to the interface objects in a different order, you'll have to search through the list in the Info window each time to select the proper outlet for the connection you're making.

    Outlet Make Connection to
    celsiusField Text field labeled Celsius
    commentsField Text object within scroll view
    commentsLabel Label that reads Notes and Itinerary for
    converter

    Instance of Converter class (cube in Instances display)

    countryField Text field labeled Country
    countryTableView

    The area underneath the Countries column

    currencyDollarsField Text field labeled Dollars
    currencyLocalField Text field labeled Local
    currencyNameField Text field labeled Currency
    currencyRateField Text field labeled Rate
    englishSpokenSwitch

    Switch (button) labeled English Widely Spoken

    fahrenheitField

    Text field labeled Fahrenheit

    languagesField

    Text field labeled Languages

    logisticsForm

    Form in group (box) labeled Logistics; the form is selected when a gray line borders all four fields.

  2. Connect the TAController instance to control objects in the interface via its actions as listed in the following table. Remember, for actions, you drag connections from the interface object to the TAController instance.

    Action Make Connection from
    addRecord:

    Add button (and also the Add item in the Records menu)

    blankFields:

    Clear button

    convertCurrency:

    Convert button to the right of the Local field

    convertTemp:

    Convert button to the right of the Fahrenheit field

    deleteRecord:

    Delete button (and also the Delete item in the Records) menu

    handleTVClick:

    The table view (you must double-click the area beneath the Countries column to select it)

    nextRecord: The Next Record item in the Records menu
    prevRecord: The Previous Record item in the Records menu
    switchClicked: The English Widely Spoken switch

View Connections in Outline Mode

The nib file window of Interface Builder gives you two modes in which to view the objects in a nib file and to make connections between those objects. So far you've been working in the icon mode of the Instances display, which pictorially represents objects such as windows and custom objects.

Outline mode, as the phrase suggests, represents objects in a hierarchical list: an outline. The advantages of outline mode are that it represents all objects as well as graphically indicating the connections between them.

You can enter outline mode from the Instances view of the MainMenu.nib window. Simply click the outline view icon directly above the view's vertical scrollbar.

As you can see in Figure 10-22, this outline view is showing the connection between the Add button on the main Travel Advisor window and the TAController instance. A connection is identified by a black line linking the two objects, as well as an icon (a crosshair for an action, an electrical outlet for outlet) and text label. In outline view, a right-pointing triangle shows connections from an object, while a left-pointing triangle shows connections to an object. You can connect objects through their outlets and actions in outline mode by Control-clicking a connection line, much as you would in icon mode.

Figure 10.22. Nib file outline view

About File's Owner

Every nib file has one owner, represented by the File's Owner icon in a nib file window. The owner is an object, external to the nib file, that relays messages between the objects unarchived from the nib file and the other objects in your application.

You specify a file's owner programmatically, in the second argument of NSBundle's loadNibNamed:owner:. The File's Owner icon in Interface Builder is a "proxy" object for that owner. Although you can assign owners to this object in Interface Builder, this doesn't necessarily guarantee anything about the file's real owner.

In the main nib file, File's Owner always represents NSApp, the global NSApplication constant. The main nib file is created automatically when you create an application project; it is loaded when an application is launched.

Nib files other than the main nib file--auxiliary nib files--contain objects and resources that an application may load only when it needs them (for example, an Info panel). You must specify the owner of auxiliary nib files.

You can determine or change the class of the current nib file's owner in Interface Builder by selecting the File's Owner icon in the nib file window and then displaying the Custom Class Info window. You'll get to practice this technique when you learn how to create multidocument applications in Chapter 11, "Cocoa's Multiple-DocumentArchitecture".

Connect the Delegate Outlet

You're now going to make TAController the delegate of the NSApp object using the File's Owner proxy. As the delegate of NSApp (the NSApplication object), TAController will receive messages from it as certain events happen.

Among many other messages, NSApp sends a message to its delegate notifying it that the application is about to terminate. Later, you will implement TAController so that, when it receives this message, it archives (saves) the dictionary containing the Country objects.

  1. Drag a connection line from File's Owner to the TAController object. Notice that the direction of the connection is from the File's Owner (which is the application object) to the TAController object.

  2. In the Connections display of the Info window, select delegate and click Connect.

Generate Source Code Files

When you generate the header and implementation files for the classes of Travel Advisor, you are finished with the Interface Builder portion of development.

  1. Save MainMenu.nib.

  2. Select the TAController class in the Classes display of the nib file window.

  3. Choose Create Files from the Classes menu.

  4. Choose the Travel Advisor project directory.

  5. Repeat for the Country class.

You don't need to do this for the Converter classes because you already imported the files from the Currency Converter project.

After you've generated the files, switch to Project Builder and make sure that the newly added files are in the Classes group, as shown in Figure 10-23. This won't affect the way Project Builder treats the files, but it helps keep the project consistently organized.

Figure 10.23. Project Builder Groups & Files

Implement the Classes of Travel Advisor

Now that the user interface for Travel Advisor has been created and the connections between objects specified in Interface Builder, you can proceed with the implementation of the application's classes.

Reuse Currency Converter

Using Interface Builder, you have already set up an action so that clicking the Convert button will invoke TAController's convertCurrency: method. Now all you have to do to is write the code to get numeric values from the user, invoke the Converter object using those values, and send the result back to the user interface for display:

  1. Open TAController.m by clicking it in Project Builder's Groups & Files view.

  2. At the top of the file, add the following line. This gives TAController access to the Converter class's methods:

    #import "Converter.h"
  3. Now modify the empty declaration of the convertCurrency: method, as shown. This is not the most compact form you can use to express a solution in Objective-C, but it clearly delineates the steps involved in solving this problem. First, the values that the user entered are retrieved from the user interface. Next, the converter object's convertAmount:atRate: method is invoked to perform the conversion operation. Finally, the value of currencyLocalField is set to reflect the result returned by the Converter object:

    - (IBAction)convertCurrency:(id)sender
    {
        float rate, dollars, result;
    
        dollars = [currencyDollarsField floatValue];
        rate = [currencyRateField floatValue];
        result = [converter convertAmount:dollars atRate:rate];
    
        [currencyLocalField setFloatValue:result];
    }

Build and Test the Application

Before moving on, go ahead and build Travel Advisor to make sure that currency conversion actually works as expected:

  1. Click the Build button in Project Builder, or type Command-B.

  2. Click the Run button to launch Travel Advisor.

  3. Enter reasonable values in the Rate and Dollars fields. Notice that the value in the Dollars field is automatically formatted with the dollar symbol.

  4. Click the Convert button next to the Local field.

Did you get a correct value back? If so, congratulations--you're already a hotshot Cocoa programmer! If nothing happened, open MainMenu.nib in Interface Builder and verify that you correctly connected the Convert button action to the TAController instance. If you got an incorrect value back, verify that you correctly connected TAController's outlets to the corresponding text fields. If you find you made a mistake, save the nib file, rebuild the application, and try again.

Implement Temperature Conversion

Implement TAController's convertTemp: method: You've already specified and connected the necessary outlets (Celsius, Fahrenheit) and action (convertTemp:), so all that remains is the method implementation. The formula you'll need is:

F = (9/5)C + 32

There's no need to implement a new Converter class for such a simple task. Simply put the code inline in TAController's convertTemp: method:

- (IBAction)convertTemp:(id)sender
{
    [fahrenheitField setFloatValue: 
            (((9.0/5.0) * [celsiusField floatValue]) + 32.0)];
}

If you have problems getting temperature conversion to work, remember to check the outlet and action connections in Interface Builder.

Implement the Country Class

The Country class is Travel Advisor's Model object, storing the information on a given country and providing methods to access as well as archive the data.

Declare instance variables

Although it has no outlets, the Country class defines a number of instance variables that correspond to the fields on Travel Advisor's user interface.

  1. In Project Builder, select Country.h.

  2. Add the declarations from Example 10-1. These instance variables hold the attribute information that describes a country. In addition to declaring instance variables, this declares that the Country class adopts the NSCoding protocol.

Example 10.1. Country Instance Variables

@interface Country : NSObject <NSCoding> 
{
    NSString *name;
    NSString *airports;
    NSString *airlines;
    NSString *transportation;
    NSString *hotels;
    NSString *languages;
    BOOL     englishSpoken;
    NSString *currencyName;
    float    currencyRate;
    NSString *comments;
}

Methods for the Country class

Now you must declare the methods that the Country class supports. These methods fall into the following three categories:

Declare the Methods

Add the method declarations for the class between the brace that closes the instance variable section and the @end statement. After the instance variables, add the declarations from the following code:

/* Initialization and De-allocation
*/
- (id)init; 
- (void)dealloc;

/* Archiving and Unarchiving */
- (void)encodeWithCoder:(NSCoder *)coder; 
- (id)initWithCoder:(NSCoder *)coder;

/* Accessor Methods */
- (NSString *)name;
- (void)setName:(NSString *)str;

- (NSString *)airports;
- (void)setAirports:(NSString *)str;

- (NSString *)airlines;
- (void)setAirlines:(NSString *)str;

- (NSString *)transportation;
- (void)setTransportation:(NSString *)str;

- (NSString *)hotels;
- (void)setHotels:(NSString *)str;

- (NSString *)languages;
- (void)setLanguages:(NSString *)str;

- (BOOL)englishSpoken;
- (void)setEnglishSpoken:(BOOL)flag;

- (NSString *)currencyName;
- (void)setCurrencyName:(NSString *)str;

- (float)currencyRate;
- (void)setCurrencyRate:(float)val;

- (NSString *)comments;
- (void)setComments:(NSString *)str;

Implement the Country object's init method

The init method first invokes super's (the superclass's) init method so that inherited instance variables will be initialized. You should always do this first in an init method. The init method also initializes the NSString instance variables to an empty string. @"" is a compiler-supported construction that creates an immutable constant NSString object from the text enclosed by the quotes. Being constants, these objects cannot be deallocated, so they do not obey the regular reference-counting technique. They do accept retain and release messages sent to them, although they are ignored, allowing these strings to be treated like regularly allocated NSStrings.

  1. Open Country.m.

  2. Fill in the implementation of the init method from Example 10-2.

Example 10.2. Implementation of Country's init Method

- (id)init
{
    [super init]; 

    [self setName:@""];
    [self setAirports:@""];
    [self setAirlines:@""];
    [self setTransportation:@""];
    [self setHotels:@""];
    [self setLanguages:@""];
    [self setCurrencyName:@""];
    [self setComments:@""];

    return self; 
}

You don't need to initialize nonobject instance variables to null values (nil, zero, NULL, and so on) because the runtime system does it for you. But you should initialize instance variables that take other starting values. Also, don't substitute nil when empty objects are expected, and vice versa. The Objective-C keyword nil represents a null "object" with an ID (value) of zero. An empty object (such as @"") is a true object; it just has no "real" content. By returning self you're returning a true instance of your object; up until this point, the instance is considered undefined.

Implement the dealloc method

In this method you release objects that you've created, copied, or retained (that don't have an impending autorelease). For the Country class, release all objects held as instance variables. If you had other retained objects, you would release them, and if you had dynamically allocated data, you would free it. When this method completes, the Country object is deallocated. The dealloc method should send dealloc to super as the last thing it does, so that the Country object isn't released by its superclass before it's had the chance to release all objects it owns.

Add the implementation of the dealloc method from Example 10-3.

Example 10.3. Implementation of Country's dealloc Method

- (void)dealloc
{
    [name release];
    [airports release];
    [airlines release];
    [transportation release];
    [hotels release];
    [languages release];
    [currencyName release];
    [comments release];

    [super dealloc]; 
}

Implement the accessor methods

For "get" accessor methods (at least when the instance variables, like Travel Advisor's, hold immutable objects), simply return the instance variable. For accessor methods that set object values, first send autorelease to the current instance variable, then copy (or retain) the passed-in value to the variable. You'll recall that the autorelease message causes the previously assigned object to be released at the end of the current event loop, keeping current references to the object valid until then.

If the instance variable has a nonobject value (such as an integer or float value), you don't need to autorelease and copy; just assign the new value.

In many situations you can send retain instead of copy to keep an object around. But for value-type objects, such as NSStrings and our Country objects, copy is better.

  1. Select Country.m in the project browser.

  2. Write the code that obtains and sets the values of the class's instance variables using the standard format shown:

    - (NSString *)name 
    {
        return name;
    }
    
    - (void)setName:(NSString *)str 
    {
        [name autorelease];
        name = [str copy];
    }

Statically Type TAController's Outlets

Interface Builder provides outlet declarations in the TAController.h file that are typed as id. Though it takes a little extra time, it's good programming practice to statically type objects unless dynamic typing is necessary.

  1. Open TAController.h and forward-declare the Converter class. Add the @class statement near the top of the file, just before the @interface statement.

    @class Converter;

    The @class directive simply lets the compiler know about the Converter class without having to include all of the declarations in the class's header file. TAController's implementation file needs access to the full class definition, so the class header file, which was added previously in this chapter, is imported there.

  2. Modify the instance variable declarations as shown in Example 10-4.

Example 10.4. TAController Instance Variables

@interface TAController : NSObject
{
    IBOutlet Converter *converter;
    IBOutlet NSTextField *countryField;
    IBOutlet NSTableView *countryTableView;

    IBOutlet NSTextField *commentsLabel;
    IBOutlet NSTextView *commentsField;

    IBOutlet NSTextField *celsiusField;
    IBOutlet NSTextField *fahrenheitField;

    IBOutlet NSTextField *currencyNameField;
    IBOutlet NSTextField *currencyDollarsField;
    IBOutlet NSTextField *currencyLocalField;
    IBOutlet NSTextField *currencyRateField;

    IBOutlet NSTextField *languagesField;
    IBOutlet NSButton *englishSpokenSwitch;

    IBOutlet NSForm *logisticsForm;
}

Add New Instance Variables to TAController.h

  1. Add the instance-variable declarations shown next. The variables countryDict and countryKeys identify the dictionary and the array used to keep track of Country objects. The Boolean recordNeedsSaving flag indicates whether the user has modified the information in any field of the user interface.

    NSMutableDictionary *countryDict; 
    NSMutableArray *countryKeys; 
    BOOL recordNeedsSaving;
  2. Add the enum declaration shown here between the last @class directive and the @interface directive. This declaration is not essential, but the enum constants provide a clear and convenient way to identify the cells in the Logistics form. Methods such as cellAtIndex: identify the editable cells in a form through zero-based indexing. This declaration gives each cell in the Logistics form a meaningful, human-readable, designation:

    enum LogisticsFormIndices {
        LGAirports=0,
        LGAirlines,
        LGTransportation,  
        LGHotels  
    };

Implement the blankFields: Method

The blankFields: method clears whatever appears in Travel Advisor's fields by inserting empty string objects and zeros. Add the implementation from Example 10-5 to TAController.m.

Example 10.5. Implementation of the blankFields: Method

- (void)blankFields:(id)sender
{
    [countryField setStringValue:@""]; 

    [[logisticsForm cellAtIndex:LGAirports] setStringValue:@""];
    [[logisticsForm cellAtIndex:LGAirlines] setStringValue:@""];
    [[logisticsForm cellAtIndex:LGTransportation] setStringValue:@""];
    [[logisticsForm cellAtIndex:LGHotels] setStringValue:@""];

    [languagesField setStringValue:@""];
    [englishSpokenSwitch setState:NSOffState]; 

    [currencyNameField setStringValue:@""];
    [currencyRateField setFloatValue:0.0];
    [currencyDollarsField setFloatValue:0.0];
    [currencyLocalField setFloatValue:0.0];

    [celsiusField setFloatValue:0.0];
    [fahrenheitField setFloatValue:0.0];

    [commentsLabel setStringValue:@"Notes and Itinerary for"]; 
    [commentsField setString:@""]; 

    [countryField selectText:self];
}

In blankFields:, the countryField is first set to an empty string. Next, the four cells of the Logistics form are cleared. Notice how the cellAtIndex: message is sent to the form object using enum constants to address each cell in the form.

The setState: message affects the appearance of two-state toggled controls, such as a switch button. With an argument of YES, the check mark appears; with an argument of NO, the check mark is removed. The setString: message sets the textual contents of NSText objects.

Recall that blankFields: is an action that is connected to the Clear button on the user interface, so you can test the method now. Build Travel Advisor, enter values for all of the fields, and click the Clear button to see if the blankFields: method is working properly. Debug if necessary.

Note that when you build the project, you'll get compiler warnings because you haven't yet supplied implementations for all of the methods declared in Country.h. These warnings can be ignored for the purposes of testing; you'll implement the missing methods later in the chapter.

TAController and Data Mediation

TAController acts as the mediator of data exchanged between a source of data and the display of that data. Data mediation involves taking data from fields in the user interface, storing it somewhere, and putting it back into the fields later. TAController has two private methods related to data mediation: populateFields: puts Country instance data into the fields of Travel Advisor's user interface, and extractFields: updates a Country object with the information in the fields.

Implement the extractFields: method

The controller's extractFields: method retrieves values from the application's user interface objects and stores the values in the current Country object's instance variables.

  1. Add the following method declaration to TAController.h:

    - (void)extractFields:(Country *)aRec;
  2. Because you've referenced a Country object, you need to forward-declare the Country class:

    @class Country;
  3. Open TAController.m and import Country.h:

    #import "Country.h"

    Although the interface file now declares the existence of a Country object, the implementation file needs to know about its methods.

  4. Enter the code from Example 10-6 for the extractFields: method in TAController.m.

Example 10.6. Implementation of the extractFields: Method

- (void)extractFields:(Country *)aRec
{
    [aRec setName:[countryField stringValue]];

    [aRec setAirports:[[logisticsForm 
            cellAtIndex:LGAirports] stringValue]];
    [aRec setAirlines:[[logisticsForm 
            cellAtIndex:LGAirlines] stringValue]];
    [aRec setTransportation:[[logisticsForm 
            cellAtIndex:LGTransportation] stringValue]];
    [aRec setHotels:[[logisticsForm 
            cellAtIndex:LGHotels] stringValue]];

    [aRec setCurrencyName:[currencyNameField stringValue]];
    [aRec setCurrencyRate:[currencyRateField floatValue]];
    [aRec setLanguages:[languagesField stringValue]];
    [aRec setEnglishSpoken:[englishSpokenSwitch state]];

    [aRec setComments:[commentsField string]];
}

Now that you have an implementation for extractFields:, test it. TAController's addRecord: method is connected to the Add button on the user interface. One of the things addRecord: will need to do is get the data from the UI, so add the call to extractFields: in the implementation of addRecord: and set a breakpoint at the invocation of extractFields::

- (IBAction)addRecord:(id)sender
{
    Country *aCountry = [[Country alloc] init];

    [self extractFields:aCountry];
}

Build and debug the app, step through the code, and see if the data you enter in the UI makes it into the Country object properly. Experiment with the use of the gdb command po to print information about an object in the gdb Console pane.

Implement the populateFields: method

The controller's populateFields: method is the inverse of extractFields:. It takes values from the current Country object's instance variables and displays them in the user interface.

  1. Add the following method declaration to TAController.h:

    - (void)populateFields:(Country *)aRec;
  2. Open the TAController.m file and enter the code from Example 10-7 for the populateFields: method.

Example 10.7. Implementation of the populateFields: Method

- (void)populateFields:(Country *)aRec
{
    [countryField setStringValue:[aRec name]]; 

    [[logisticsForm cellAtIndex:LGAirports] setStringValue:
            [aRec airports]]; 
    [[logisticsForm cellAtIndex:LGAirlines] setStringValue:
            [aRec airlines]];
    [[logisticsForm cellAtIndex:LGTransportation] setStringValue:
            [aRec transportation]];
    [[logisticsForm cellAtIndex:LGHotels] setStringValue:
            [aRec hotels]];

    [currencyNameField setStringValue:[aRec currencyName]];
    [currencyRateField setFloatValue:[aRec currencyRate]];
    [languagesField setStringValue:[aRec languages]];
    [englishSpokenSwitch setState:[aRec englishSpoken]];

    [commentsLabel setStringValue:[NSString stringWithFormat:
            @"Notes and Itinerary for %@", [aRec name]]];
    [commentsField setString:[aRec comments]]; 

    [countryField selectText:self]; 
}

The first thing populateFields: does is display the name of the current country in the Country field. The value is retrieved from the name instance variable of the Country record (aRec) passed into the populateFields: method. The object returned by the expression [aRecname] is used as the argument of the setStringValue: method, which sets the text content of the receiver (in this case, the countryField object). Next, the remainder of the user interface elements are updated. Finally, the selectText: message is sent to Country field so that any text is selected, or if there is no text, the cursor is inserted into the field.

Get the Table View to Work

The table view in Travel Advisor has only one column and is used to display the list of countries for which the application contains travel information. You've already explored table views and data sources in Chapter 9, "Data Functionality", so the steps in this section of the tutorial should be straightforward.

Implement the behavior of the table view's data source

Implement TAController's awakeFromNib method. Designate self as the data source:

- (void)awakeFromNib
{
    [countryTableView setDataSource:self];
    [countryTableView sizeLastColumnToFit];
}

The [countryTableView setDataSource:self] message identifies the TAController object as the table view's data source. The table view will commence sending NSTableDataSource messages to TAController. (You can effect the same thing by setting the NSTableView's dataSource outlet in Interface Builder.)

Implement two methods of the NSTableDataSourceinformal protocol

To fulfill its role as data source, TAController must implement two methods of the NSTableDataSource informal protocol: numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:, as shown in Example 10-8.

Example 10.8. Implementation of the NSTableDataSource Protocol

- (int)numberOfRowsInTableView:(NSTableView *)theTableView { 
    return [countryKeys count];
}

- (id)tableView:(NSTableView *)theTableView 
    objectValueForTableColumn:(NSTableColumn *)theColumn
    row:(int)rowIndex
{
    if ([[theColumn identifier] isEqualToString:@"Countries"])
        return [countryKeys objectAtIndex:rowIndex];
    else
        return nil;
}

The first method returns the number of country names in the countryKeys array. The table view uses this information to determine how many rows to create.

The second method evaluates the column identifier to determine if it's the correct column (it should always be Countries). If it is, the method returns the country name from the countryKeys array that is associated with rowIndex. This name is then displayed at rowIndex of the column. (Remember, the array and the cells of the column are synchronized in terms of their indexes.)

The NSTableDataSource informal protocol has another method, tableView:setObjectValue:forTableColumn:row:, which you won't implement in this tutorial. This method allows the data source to extract data entered by users into table view cells; since Travel Advisor's table view is read-only, there is no need to implement it.

If you had an application with multiple table views, each table view would invoke these NSTableView delegation methods (as well as the others). By evaluating the theTableView argument, you could distinguish which table view was involved.

Implement the country-selection method

Finally, you have to have the table view respond to mouse clicks in it, which indicate a request that a new record be displayed. As you recall, you defined in Interface Builder the handleTVClick: action for this purpose. This method must do a number of things:

Example 10.9. Implementation of the handleTVClick: Method

- (IBAction)handleTVClick:(id)sender
{
    Country *aRec;
    NSString *countryName;
    int index = [sender selectedRow];

    if (index == -1) return;
    countryName = [countryKeys objectAtIndex:index];

    if (recordNeedsSaving) {
        [self addRecord:self];
        index = [countryKeys indexOfObject:countryName];
        [countryTableView selectRow:index byExtendingSelection:NO];
    }

    aRec = [countryDict objectForKey:countryName];
    [self populateFields:aRec];
}

The first thing handleTVClick: does is identify which row (and hence country) the user selected. If no row is selected, NSTableView's selectedRow method returns -1 and handleTVClick: exits. Otherwise, the row index is used to get the country's name.

When any Country-object data is added or altered, Travel Advisor sets the recordNeedsSaving flag to YES (you'll learn how to do this later on). If recordNeedsSaving is YES, the code calls the addRecord: method, which will update a modified record or insert a new one, as appropriate. Because inserting a new record will alter the contents of the table, the selected row may need to be updated to highlight the correct country. To do this, the current position of the country's name within countryKeys is obtained, using the indexOfObject: method. The corresponding row is then selected within the table view.

Finally, the country's name is used as the key to get the associated Country instance from the dictionary. It then calls populateFields: to update the window with the country's instance-variable values.

Optional exercise

Users often like to have key alternatives to mouse actions such as clicking a table view. One way of acquiring a key alternative is to add a menu command in Interface Builder, specify a key as an attribute of the command, define an action method that the command will invoke, and then implement that method.

The methods nextRecord: and prevRecord: should be invoked when users choose Next or Previous from the Records menu or type the key equivalents Command-Option-N and Command-Option-P. In TAController.m, implement these methods, keeping the following hints in mind:

  1. Get the index of the selected row (selectedRow).

  2. Increment or decrement this index, according to which key is pressed (or which command is clicked).

  3. If the start or end of the table view is encountered, "wrap" the selection. (Hint: use the count of the countryKeys array.)

  4. Using the index, select the new row, but don't extend the selection.

  5. Simulate a mouse click on the new row by sending handleTVClick: to self.

Build the Project

Now is a good time to take a break and build Travel Advisor. See if there are any errors in your code or in the nib file.

Add and Delete Records

When users click Add Record to enter a Country record, the addRecord: method is invoked. You want this method to do a few things besides adding a Country object to the application's dictionary:

Field Validation

The NSControl class gives you an API for validating the contents of cells. Validation verifies that the values of cells fall within certain limits or meet certain criteria. In Travel Advisor, we want to make sure that the user does not enter a negative value in the Rate field.

The request for validation is a message, control:isValidObject:, that a control sends to its delegate. The control, in this case, is the Rate field.

  1. In awakeFromNib, make TAController a delegate of the field to be validated: the Rate field:

    [currencyRateField setDelegate:self];
  2. Implement the control:isValidObject: method to validate the value of the field:

    - (BOOL)control:(NSControl *)control isValidObject:(id)obj
    { 
        if (control == currencyRateField) {  
            if ([obj floatValue] <= 0.0) {
                NSRunAlertPanel(@"Travel Advisor", 
                    @"Rate cannot be zero or negative.", nil, nil, nil);
                return NO;
            }
        }
        return YES;
    }

Because you might have more than one field's value to validate, this example first determines which field is sending the message. It then checks the field's value (passed in as the second object); if it is negative, it displays a message box and returns NO, blocking the entry of the value. Otherwise, it returns YES and the field accepts the value.

The previous example calls NSRunAlertPanel simply to inform the user why the value cannot be accepted. Although Travel Advisor doesn't evaluate it, the function returns a constant indicating which button the user clicks in the message box. The logic of your code could therefore branch according to user input. In addition, the function allows you to insert variable information (using printf-style conversion specifiers) into the body of the message.

Application Management

At this point you've finished the major coding tasks for Travel Advisor. All that remains to be implemented are a half dozen or so methods. Some of these methods perform tasks that every application should do. Others provide bits of functionality that Travel Advisor requires. In this section you'll:

The data that users enter into Travel Advisor should be saved in the file system or archived. The best time to initiate archiving in Travel Advisor is when the application is about to terminate. Earlier you made TAController the delegate of the application object (NSApp). Now respond to the delegate message applicationShouldTerminate:, which is sent just before the application terminates.

Implement the delegate method applicationShouldTerminate:, as shown in Example 10-11.

Example 10.11. Implementation of the applicationShouldTerminate: Method

- (NSApplicationTerminateReply)applicationShouldTerminate:(id)sender
{  
   NSString *storePath = [NSHomeDirectory()
            stringByAppendingPathComponent:@"Documents/TravelData.travela"];

   // save current record if it is new or changed
   [self addRecord:self]; 
   if (countryDict)
      [NSArchiver archiveRootObject:countryDict toFile:storePath];

   return NSTerminateNow;
}

This function constructs a pathname for the archive file, TravelData. This file is stored in the Documents folder of the user's home directory.

If the countryDict dictionary exists, TAController archives it with the NSArchiver class method archiveRootObject:toFile:. Since the dictionary is designated as the root object for archiving, all objects that the dictionary references (that is, the Country objects it contains) will be archived too.

Implement TAController's methods for initializingand deallocating itself

Implement the init and dealloc methods from Example 10-12.

Example 10.12. Implementation of the init and dealloc Methods

- (id)init
{
    NSString *storePath = [NSHomeDirectory()
            stringByAppendingPathComponent:@"Documents/TravelData.travela"];
    [super init];

    countryDict = [NSUnarchiver unarchiveObjectWithFile:storePath];

    if (!countryDict) {
        countryDict = [[NSMutableDictionary alloc] init];
        countryKeys = [[NSMutableArray alloc] initWithCapacity:10];
    } else {
        countryDict = [[NSMutableDictionary alloc]
                initWithDictionary:countryDict];
        countryKeys = [[NSMutableArray alloc]
                initWithArray:[[countryDict allKeys]
                sortedArrayUsingSelector:
                @selector(caseInsensitiveCompare:)]];
    }

    recordNeedsSaving = NO;
    return self;
}

- (void)dealloc
{
    [countryDict release];
    [countryKeys release];
    [super dealloc];
}

The init method locates the archive file TravelData in the user's Documents directory and returns the path to it.

The unarchiveObjectWithFile: message unarchives (that is, restores) the object whose attributes are encoded in the specified file. The object that is unarchived and returned is the NSDictionary of Country objects (countryDict).

If no NSDictionary is unarchived, the countryDict instance variable remains nil. If this is the case, TAController creates an empty countryDict dictionary and an empty countryKeys array. Otherwise, it retains the instance variable and builds the country keys array.

The [countryDict allKeys] message returns an array of keys (country names) from countryDict, the unarchived dictionary that contains Country objects as values. The sortedArrayUsingSelector: message sorts the items in this "raw" array using the caseInsensitiveCompare: method defined by the class of the objects in the array, in this case NSString (this is an example of polymorphism and dynamic binding). The sorted names go into a temporary (autoreleased) NSArray--since that is the type of the returned value--and this temporary array is used to create a mutable array, which is then assigned to countryKeys. A mutable array is necessary because users may add or delete countries.

The dealloc method releases the objects created by the init method. It then calls its superclass's implementation of dealloc to continue the process until the object itself is freed.

Implement notification to track modified records

When users modify data in fields of Travel Advisor, you want to mark the current record as modified so later you'll know to save it. The Application Kit broadcasts a notification whenever text in the application is altered. To receive this notification, add TAController to the list of the notification's observers.

  1. In the awakeFromNib method, make TAController an observer of all objects posting NSControlTextDidChangeNotification:

    [[NSNotificationCenter defaultCenter] addObserver:self
             selector:@selector(textDidChange:)
             name:NSControlTextDidChangeNotification object:nil];
  2. You also need to observe the notes field; it isn't a control text object:

    [[NSNotificationCenter defaultCenter] addObserver:self
             selector:@selector(textDidChange:)
             name:NSTextDidChangeNotification object:commentsField];
  3. Implement textDidChange: to set the recordNeedsSaving flag. Two of the editable fields of Travel Advisor hold temporary values used in conversions and so are not saved. The if statement checks if these fields are the ones originating the notification and, if they are, returns without setting the flag. (The object message obtains the object associated with the notification.)

    - (void)textDidChange:(NSNotification *)notification
    {
        if (([notification object] == currencyDollarsField) ||
            ([notification object] == celsiusField)) return;
        
        recordNeedsSaving = YES;
    }
  4. Implement the switchClicked: action method to learn of changes to the English Widely Spoken switch:

    - (IBAction)switchClicked:(id)sender
    {
        recordNeedsSaving = YES;
    }

Implement Archiving and Unarchiving

In this section you'll implement the methods for archiving and unarchiving the Country class. Once this step is complete, the application will be able to save the entire dictionary of countries to disk, so you won't lose your travel information when the application terminates.

  1. Implement the encodeWithCoder: method in Country.m as shown:

    - (void)encodeWithCoder:(NSCoder *)coder
    {
        [coder encodeObject:[self name]]]; 
        [coder encodeObject:[self airports]];
        [coder encodeObject:[self airlines]];
        [coder encodeObject:[self transportation]];
        [coder encodeObject:[self hotels]];
        [coder encodeObject:[self languages]];
        [coder encodeValueOfObjCType:"s" at:&englishSpoken]; 
        [coder encodeObject:[self currencyName]];
        [coder encodeValueOfObjCType:"f" at:&currencyRate];
        [coder encodeObject:[self comments]];
    }
  2. Implement the initWithCoder: method as shown:

    - (id)initWithCoder:(NSCoder *)coder
    {
        [self setName:[coder decodeObject]]; 
        [self setAirports:[coder decodeObject]];
        [self setAirlines:[coder decodeObject]];
        [self setTransportation:[coder decodeObject]];
        [self setHotels:[coder decodeObject]];
        [self setLanguages:[coder decodeObject]];
        [coder decodeValueOfObjCType:"s" at:&englishSpoken];
        [self setCurrencyName:[coder decodeObject]];
        [coder decodeValueOfObjCType:"f" at:&currencyRate];
        [self setComments:[coder decodeObject]];
    
        return self; 
     }

Build and Run the Application

When Travel Advisor is finished building, start it up by double-clicking the icon in the Finder. Then put the application through the following tests:

Back to: Sample Chapter Index

Back to: Learning Cocoa


O'Reilly Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com