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.
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:
Open TAController.m
by clicking it
in Project Builder’s Groups & Files view.
At the top of the file, add the following line. This gives TAController access to the Converter class’s methods:
#import "Converter.h"
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]; }
Before moving on, go ahead and build Travel Advisor to make sure that currency conversion actually works as expected:
Click the Build button in Project Builder, or type Command-B.
Click the Run button to launch Travel Advisor.
Enter reasonable values in the Rate and Dollars fields. Notice that the value in the Dollars field is automatically formatted with the dollar symbol.
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 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.
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.
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.
In Project Builder, select
Country.h
.
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.
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
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;
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.
Open Country.m
.
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.
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.
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.
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.
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.
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 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;
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 };
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 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.
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.
Add the following method declaration to TAController.h
:
- (void)extractFields:(Country *)aRec;
Because you’ve referenced a Country object, you need to forward-declare the Country class:
@class Country;
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.
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.
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.
Add the following method declaration to
TAController.h
:
- (void)populateFields:(Country *)aRec;
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.
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 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.)
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.
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.
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:
Get the index of the selected row
(selectedRow
).
Increment or decrement this index, according to which key is pressed (or which command is clicked).
If the start or end of the table view is encountered, “wrap”
the selection. (Hint: use the count of the countryKeys
array.)
Using the index, select the new row, but don’t extend the selection.
Simulate a mouse click on the new row by sending
handleTVClick:
to self
.
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.
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.
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.
In awakeFromNib
, make TAController a delegate
of the field to be validated: the Rate field:
[currencyRateField setDelegate:self];
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.
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 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.
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.
In the awakeFromNib
method, make TAController an
observer of all objects posting
NSControlTextDidChangeNotification
:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:NSControlTextDidChangeNotification object:nil];
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];
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; }
Implement the switchClicked:
action method to learn
of changes to the English Widely Spoken switch:
- (IBAction)switchClicked:(id)sender { recordNeedsSaving = YES; }
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.
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:¤cyRate]; [coder encodeObject:[self comments]]; }
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:¤cyRate]; [self setComments:[coder decodeObject]]; return self; }
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.
52.15.197.143