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:

  • Object initialization and deallocation

  • Object archiving and unarchiving

  • Accessor methods

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, 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 NSTableDataSource informal 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:

  • Save the current Country object or create a new one.

  • If there’s a new record, resort the array providing data to the table view.

  • Display the selected record.

  • Implement the handleTVClick: Method from Example 10.9. This method responds to user selections in the table view.

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:

  • Ensure that a country name has been entered.

  • Make the table view reflect the new record.

  • If the record already exists, update it (but only if it’s been modified).

  • Implement the addRecord: method from Example 10.10.

    Example 10-10. Implementation of the addRecord: Method

    - (IBAction)addRecord:(id)sender
    {
        Country *aCountry;
        NSString *countryName = [countryField stringValue];
    
        //  Is there country data to be saved?
            if (recordNeedsSaving && ![countryName isEqualToString:@""])
    {
            aCountry = [countryDict objectForKey:countryName];
    
            //  Is current object already in dictionary?
            //  ...  aCountry will be nil if new
                if (!aCountry) {
                //  Create Country obj, add to dict, add name to keys array
                aCountry = [[[Country alloc] init] autorelease];
                [countryDict setObject:aCountry forKey:countryName];
                [countryKeys addObject:countryName];
    
                //  Sort array and update table view
                [countryKeys sortUsingSelector:@selector(compare:)];
                [countryTableView reloadData];
                [countryTableView selectRow:
                        [countryKeys indexOfObject:countryName]
                        byExtendingSelection:NO];
            }
    
            //  Update the country object
            [self extractFields:aCountry];
            recordNeedsSaving = NO;
    
            [commentsLabel setStringValue:[NSString stringWithFormat:
                    @"Notes and Itinerary for %@", countryName]];
            [countryField selectText:self];
        }
    }

    This method adds a Country object to the NSDictionary “database.” This section of code verifies that a country name has been entered and that its fields are flagged as modified. It then sees if there is a Country object with the given name in the dictionary. If there’s no object for the key, objectForKey: returns nil. In this case, the code creates a new Country object and adds it to the dictionary and adds its name to the keys array. The array is then sorted and the table view is updated. The reloadData message forces the table view to update its contents. The selectRow:byExtendingSelection: message highlights the new record in the table view.

    Immediately following the innermost if block, the aCountry variable is a reference to an object stored in countryDict. This Country object is updated with the information in the application’s fields (extractFields:) and the recordNeedsSaving flag is reset. Finally, the label over the text view is updated to reflect the just-added country and the country name field is highlighted.

    In Example 10.10, note the expression if (!aCountry). For objects, this is shorthand for if (aCountry == nil); in the same vein, if (aCountry) is equivalent to if (aCountry != nil). Also note that the newly created Country object is sent an autorelease message when created. Because Country objects are being stored within a dictionary, which retains its values, addRecord: does not want ownership of the newly created Country object. By autoreleasing it, the dictionary will be the sole owner of the object at the end of the current event loop.

  • Implement the deleteRecord: method. Although similar in structure to addRecord:, this method is a little simpler because you don’t need to worry about whether a Country record has been modified. Once you’ve deleted the record, remember to update the table view and clear the fields of the application.

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:

  • Archive and unarchive the TAController object.

  • Implement TAController’s init and dealloc methods.

  • Save data when the application terminates.

  • Mark the current record when users make a change.

  • Obtain and display converted currency values.

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 initializing and 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:

  • Enter a few records. Make up geographical information if you have to—you’re not trusting your future travels to this application. Not yet, anyway.

  • Click the items in the table view and notice how the selected records are displayed. Press Command-Option-N and Command-Option-P and observe what happens.

  • Enter values in the conversion fields to see how they’re automatically formatted. Try to enter a negative value in the Rate field.

  • Quit the application and then start it up again. Notice how the application displays the same records that you entered.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.197.143