macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Working with Tables: Writing an Address Book Application
Pages: 1, 2, 3, 4

The action methods

The way our application will work is that the user will enter the information for a new individual into the provided text fields and then click "Add" to add the new record to the end of the list, or "Insert" to insert the record immediately before the currently selected row in the table. If the users want to delete a record, they will highlight the row in the table and click the Delete button.



Notice that two of these actions involve creating a new record from the input data. Rather than write redundant code in addRecord and insertRecord, we will factor out the common code into a new method called createRecord. By doing this, our code will be more maintainable and less error-prone under modification. That is, rather than going to each method that creates a record, now we only have to change the code in one central location.

What createRecord does is create a new record dictionary from the values entered in the text fields, and returns this dictionary to the sender of createRecord. We're going to use the NSMutableDictionary method setObject:forKey: to add a key-value pair to a dictionary. Let's see how this looks in code:

-(NSDictionary *)createRecord
{
  NSMutableDictionary *record = [[NSMutableDictionary alloc] init];

  [record setObject:[firstNameField stringValue] forKey:@"First Name"];
  [record setObject:[lastNameField stringValue] forKey:@"Last Name"];
    [record setObject:[emailField stringValue] forKey:@"Email"];
  [record setObject:[homePhoneField stringValue] forKey:@"Home Phone"];

  [record autorelease];
  return record;
}

In the first line, we created a new empty dictionary using alloc and init -- nothing new there. In the next block of four lines, we add the string from each text field, along with an appropriate key, to the dictionary.

Because we used alloc to create our new dictionary, we have to make sure to release it. But because we want to return and let the sender of createRecord assert ownership on the returned dictionary, we have to autorelease it. Or, we could have used an NSDictionary or NSMutableDictionary convenience constructor in place of alloc or init and [record autorelease] because objects returned by convenience constructors are autoreleased. This version of the code looks like this:

-(NSDictionary *)createRecord
{
  NSMutableDictionary *record = [NSMutableDictionary dictionary];

  [record setObject:[firstNameField stringValue] forKey:@"First Name"];
  [record setObject:[lastNameField stringValue] forKey:@"Last Name"];
    [record setObject:[emailField stringValue] forKey:@"Email"];
  [record setObject:[homePhoneField stringValue] forKey:@"Home Phone"];

  return record;
}

Here we used the convenience constructor defined in NSDictionary (and inherited by NSMutableDictionary), +dictionary, which just returns an empty dictionary.

addRecord

Now let's look at the first of our action methods. addRecord: is going to use the NSMutableDictionary method addObject:, which adds the object in the argument to the end of the receiver array (as is suggested by the name). The object we're going to add to records is the dictionary returned by createRecord.

-(IBAction)addRecord:(id)sender
{
    [records addObject:[self createRecord]];
    [tableView reloadData];
}

Notice the last line. Whenever we want to update the contents of a table to reflect changes in the internal data, we send the table view object a reloadData message, which does just that. We want to do this every time we change something about the data, so we'll see reloadData in the three of these action methods.

Previously I said that the sender of createMethod had to assert ownership of the returned dictionary before the autorelease pool is released, or it will disappear. So where does that happen in addRecord? We don't retain it in any way, rather records retains it.

When objects are added to an array, the receiving array sends a retain message to the object being added. In that way, the array asserts ownership over the object being added, which in addRecord is the dictionary from createRecord, and the object won't be de-allocated with the autorelease pool. Objects that are members of an array are released when the parent array is released.

insertRecord

The insertRecord method is similar to addRecord:, except we use the NSMutableArray method insertObject:atIndex: instead of addObject:.

insertObject:atIndex takes two arguments. Tthe first is the object we wish to add to the array. We will do the same thing here as we did before: Use the return value of createObject as the first argument.

The second argument is the array index where we want our object to be put. Inserting an object at some index places it before the object that previously occupied that index. But where do we get the index for this method? We said before that we want insertRecord to insert a new record before the row currently highlighted in the table view. Luckily, we can get the index of a selected row in a table view by sending a selectedRow message to the table view:

-(IBAction)insertRecord:(id)sender
{
  int index = [tableView selectedRow];
  [records insertObject:[self createRecord] atIndex:index];
  [tableView reloadData];
}

And that's that.

deleteRecord and friends

The deleteRecord method operates on the same principle of getting the index of the selected row in the table as we did above. However, this time we'll use the NSMutableArray method, removeObjectAtIndex:, which simply removes from the array the object at the index indicated in the argument. Objects at indexes beyond the one we're deleting will shift down to fill the open space. Here's how version 1 of deleteRecord looks in code:

-(IBAction)deleteRecord:(id)sender
{
  int index = [tableView selectedRow];
  [records removeObjectAtIndex:index];
  [tableView reloadData];
}

This implementation could be expanded to remove the limitation of only being able to delete records one at a time. To do this, we have to use what's known as an enumerator, which is an instance of the Foundation class NSEnumerator.

NSEnumerator

An enumerator is an object created by the objectEnumerator and other certain methods of the collection classes, such as NSArray, NSDictionary, and NSSet). What an enumerator does is go through the collection object that created it and return its member objects one by one (NSEnumerator is analogous to the Java "Iterator" class, for you Java folks). The way we get an object from a collection's enumerator is by sending the enumerator a nextObject message. We can continue to send nextObject messages to return the objects from the collection that have yet to be enumerated. When all of the objects of a collection have been returned by the enumerator, subsequent nextObject messages return "nil".

Enumerators, in short, are an object-oriented way of implementing the timeless for loop that we often use to iterate through an array. Consider these two pieces of code that allow us to iterate through enumerating an array. First, the for loop implementation:

// assume anArray exists
id object;
int i;

for (i = 0; i < [anArray count]; i++ ) {
    object = [anArray objectAtIndex:i];
    // do something with object
}

And the NSEnumerator way:

// again, assume anArray exists
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;

while ( (identifier = [enumerator nextObject]) ) {
    // do something with object
}

What we did was to send anArray an objectEnumerator message which returns an enumerator of anArray. In the conditional of the while loop, we use nextObject to get the next object in anArray, and store that in the variable object to be used within the while loop.

We said that after the enumerator reaches the end of the array, nil is returned by nextObject, which in a conditional statement evaluates to "false"; we use that fact to shut down the while loop after all the collection members have been enumerated. Think of an enumerator as a role call for the members of a collection, like a role call for people in class. Of course, we have perfect attendance with enumerators. Bueller? Bueller? Anybody?

You might be asking yourself why we would want to use this glorified way of iterating through an array when the for loop works just fine. The answer to this question is that for loops do not work just fine. They have severe limitations when it comes to object-oriented software design.

Notice in the first example where we used the for loop, how we used the NSArray method, objectAtIndex, to obtain the object we want to use from the collection and find out how many member objects were contained in the array. What if we decided that rather than using an array to store a bunch of objects, we wanted to use a dictionary or a set? (NSSet is another Cocoa collection, but we won't talk about it in this column.) Then we couldn't use objectAtIndex to access the contents of the collection because not every collection indexes its members like arrays do.

So if we decided to change our collection data structure from the benign array to a dictionary, set, or even a collection of our own concoction, we would have to pore through our code and determine every place where we use NSArray-specific code -- such as when we iterate through the collection -- and change the code appropriately. That would stink.

The object-oriented solution to this problem is to provide an abstract class that provides a simple interface for enumerating collections -- NSEnumerator. As an abstract class, NSEnumerator's only job is to declare the operations that concrete subclasses must implement to adhere to the NSEnumerator interface. Hidden from our view in the frameworks are several concrete subclasses of NSEnumerator that implement the machinery to enumerate many different things such as dictionaries, arrays, sets -- you name it. The important thing in all of this is that the concrete enumerators all stick to the interface defined by the abstract superclass, NSEnumerator.

Our job is to make sure that we program to an interface, not an implementation. Put another way, don't write code that accesses the guts and nuts and bolts of a class (the implementation). Rather, try as often as you can to interact with a class only through its interface. The for loop iteration method relied on implementation information about the internal structure of NSArray by using objectAtIndex:, and count; that particular for loop will only work with NSArray.

The code that used NSEnumerator, however, will not break because we don't rely on any information about the collections implementation. We use only the nextObject method declared in NSEnumerator's interface. If we decide to implement our own collection class, all we have to do is subclass NSEnumerator and implement nextObject to work with our custom collection.

In this way, the responsibility of making sure an application works falls to the implementer of a new data structure. Its task of adhering to the NSEnumerator interface is very localized, rather than the implementer of the client code that accesses the collection, which might have to change a thousand for loops to make the application work right with a new collection. I hope you can see the utility in this. It makes code much more maintainable, and that is one of the major goals and advantages of object-oriented programming.

So how does this business of enumerators relate to table views and deleting groups of records? We saw already how the NSTableView method, selectedRow, returns the index of the currently selected row. Suppose we select multiple rows then? How do we get the indexes of those rows?

The answer to this is by using another NSTableView method: selectedRowEnumerator. What this method does is return an NSEnumerator object that lets us enumerate the selected rows. Each time we send the returned enumerator a nextObject message, we get in return an NSNumber representation of a row's index. One way we might go about modifying deleteRecord to incorporate multiple record deletion is the following, version two of deleteRecord:

-(IBAction)deleteRecord:(id)sender
{
  NSEnumerator *enumerator = [tableView selectedRowEnumerator];
  NSNumber *index;

  while ( (index = [enumerator nextObject]) ) {
    [records removeObjectAtIndex:[index intValue]];
  }

  [tableView reloadData];
}

What we first did was send tableView a selectedRowEnumerator message, then we stored the returned enumerator in the variable enumerator. We also declared an NSNumber variable, "index", to store the row index returned by nextObject each time through the loop.

You can see in the while loop conditional statement how we obtain the next row index by sending a nextObject message to enumerator, and then storing that in index. NSNumber is a simple class; nothing more really than a wrapper for standard C number types. The argument of removeObjectAtIndex: is an unsigned int, and cannot accept NSNumber objects as an argument value. We can resolve this easily enough by sending an intValue message to the NSNumber object, which returns a plain-Jane C int, and using that int in our argument. After the loop exits (by nextObject returning nil), we carry on by telling tableView to update its contents

.

Pages: 1, 2, 3, 4

Next Pagearrow