MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Working with Tables: Writing an Address Book Application

08/10/2001

One of the most common means of displaying and organizing data in an interface is with a table. We see this in spreadsheets, checkbook registers, train timetables, address books, and even the Finder. Tables are, after all, one of the most versatile and efficient ways to display collections of data. In this column, we will build a simple address book application centered on a table so that we can learn how to implement tables using Cocoa.

The Cocoa class that provides us with table functionality is NSTableView. NSTableView takes care of everything needed for a table such as managing table columns, drawing the table, retrieving data from data sources, displaying that data as the table contents, and much more. Putting together a table is relatively simple, although by no means obvious to the beginning Cocoa developer -- there are quite a few pieces to fit together. I spent the better part of a weekend last winter pulling out my hair when I first was trying to figure out how this all worked.

This column is a long one, so get comfortable. We're going to build this application using an interface to code and build. We'll first build and layout our GUI, then we'll create an appropriate controller class, and then wire the interface to the controller object. While in Interface Builder, we will also spend a bit of time setting up and configuring our table. Then we will go back to Project Builder and lay down the essential code needed to make this application run.

Building the interface

Before we start the interface, we need to first create a new project. So fire up Project Builder and create one. The project type will be a Cocoa Application (not document-based, mind you), and we will name this project "AddressBook". With a new project in hand, double-click MainMenu.nib under the Resources group to open the AddressBook interface in Interface Builder.

The GUI for our application consists of the following elements:

Table views are found in the "Cocoa Tabulation Views" palette. This palette contains four different interface components: a browser view, a outline view, a tabbed view, and a table view. The table view is shown highlighted in the upper right of the figure below.

Screenshot.
The NSTableView object is shown highlighted in the Cocoa Tabulation Views palette.

To use it, simply drag it from the palette onto the interface. Go ahead and drag the other components listed above onto the window. Name the Message Text fields "First Name", "Last Name", "Email", and "Home Phone", and name the three buttons "Add", "Insert", "Delete". In the image below you can see how I've set up my interface for this application (for now, don't worry about the column header names in the table view; we'll get to that soon enough).

Screenshot.
This is how I set up my interface. You can rearrange yours in whatever way suits your fancy (just make sure you get all of the connections right later on).

The next step is to create a controller object. Remember that we do this by going to the Classes pane of nib file window and subclassing NSObject. (You can easily subclass by pressing return with NSObject selected). Name your controller object "Controller". Our application interface requires the five outlets listed below:

and these three actions, one for each button:

Comment on this articleMike covered a lot of territory in this tutorial on using tables and writing an address book app. What are the larger issues at play here?
Post your comments

Also in Programming With Cocoa:

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Add these actions and outlets to Controller. Now instantiate Controller by selecting it in the Classes list and choosing "Instantiate" from the Classes menu.

With our new instance of Controller, we're ready to make the connections between Controller's outlets and actions and our interface. Recall that we wire connections by "control-dragging" from one object to another in the direction that messages would flow between the two. Wiring a table view is no different than wiring any other object. Go ahead and wire things up at this point, and save your work when you finish.

Once we've made all of the necessary connections between the Controller and our interface objects go back to the Classes tab, choose the Controller class, and select from the Classes menu "Create Files" to add the interface and implementation files for the Controller class to our project.

With that our interface is nearly finished. The last thing we need to do is configure and tweak our table view to fit our application.


Setting up a table

Learning CocoaLearning Cocoa
By Apple Computer, Inc.
Table of Contents
Index
Sample Chapter
Full Description
Read Online -- Safari

To configure our table view, open Interface Builder's Info panel. This can be found under the Tools menu with Show Info, or by hitting shift-command-i. Select the table view to display its attributes in the Info panel; if the attributes are not shown, select "Attributes" from the pop-up menu at the top of the Info panel.

What we want to do first is change the number of columns. In the # Colms: field enter "4", one for each of the fields we've decided to have in our address book. If you can't see all of the columns after changing the number of columns, you might need to resize them so more can fit in the view. Double-clicking on the table view will select it in such a way that allows us to change the size of the columns by dragging the dividing lines between the column headers. When you select a table view by double-clicking there will be a heavy outline around the table view, and it will also be shaded.

While we're still editing the table view's attributes, make sure that in the "Selection" box the options "Allows Empty Selection", "Allows Multiple Selection", and "Allows Column Selection" are all checked.

Next on the docket for our table is to name our columns. After double-clicking on the table (like we just did to resize the columns), you will be able to select individual columns by clicking on each column's header. When you do this, the Info panel will change to show attributes for the selected column (which is an instance of NSTableColumn), rather than table as a whole (NSTableView). Here we must do two things: set the name that will appear in the column header, and give each column a unique identifier. The column identifier is a string that we will use in the code to determine which column is which. Now set up all of your columns.

I gave my columns the same names as the text field labels in the interface -- "First Name", "Last Name", "Email", and "Home Phone" -- and made the identifiers for each column the same. In NSTableColumn info, you can also set how you want text to be aligned in the column headers, and in the data cells themselves. Now would be a good time to save your work and double check that we set all of the column identifiers correctly. It's also not such a bad idea to really be careful and double-check your work in Interface Builder; this is probably one of the last places you'll look for errors when your application isn't acting behaving properly (for me, anyway).

Before we close up shop in Interface Builder, we've got one last very important thing to do. For the table view to work properly, it has to be able to go somewhere to get the data that its going to display -- a data source object. Interface Builder has provisions for connecting a table view to a data source object in the same way we wired the interface to actions and outlets.

In this application, we'll make Controller the data source. To make this connection, first make sure the table view is double-click selected, and then drag a wire from the table view object to the Controller object, and in the Connections panel highlight "dataSource" in the list of outlets (yes, it's just an outlet, nothing too fancy), and make the connection by clicking on the Connect button.

Save your work again in Interface Builder, and we'll head back to Project Builder to lay down our code.

Coding the address book

Coding AddressBook involves three pieces. The first thing we need to do is set up some sort of data structure to store our address book entries. Then we'll implement our action methods, and finally we will enable Controller to work as a proper table view data source. Let's start with a discussion about how we'll store data, and how to set this up in code.

Dictionaries and arrays

In constructing a data structure for this application, we have to consider that we have two levels of data storage. On one level, we have to deal with the individual entries (the records) that store a person's first and last name, their email address, home phone number, and anything else we may decide to store in the future. On another level, we have to collect all of the individual records and store them in an efficient and convenient manner.

One way we could organize the data of a record is with an array where each index corresponds to a different field -- index 0 = first name, index 1= last name, and so forth. However, this can be cumbersome because we can't directly refer to the record fields by name. If we wanted a piece of information from a record, we would have to first figure out what index we need to recall, and then get the data at that index. A better way of storing this information would be to just tell the record to return the "First Name", or "Home Phone", or whatever. It just so happens that we have a data structure that will do this for us: Cocoa's NSDictionary class.

A dictionary stores data as an unordered collection of key-value pairs. What this means is that each piece of data in our dictionary has a key (some unique identifier) that we use to retrieve the associated data value. For example, in a real, book-like dictionary, the keys are the words in bold that we look up, and the associated value is the grammatical information and definition for that word. We find the definition of a word by looking up its key, and then looking at the definition. A phone book is another example of this. The name of a person is the key, and the phone number and address is the value.

A data dictionary works in the same way, except we don't have to flip through pages and pages of irrelevant information to find what we're looking for, which is decidedly inefficient. Data dictionaries handle that sort of thing much better. One of the powerful things about NSDictionary is that the key is typed to id, which means we can make the key any type of object that we want. The same goes for the stored value-type id; we can store anything in a dictionary that is an object. This opens up the way for extremely clever and powerful solutions to problems. AddressBook will simply use strings, but more esoteric applications might use any sort of custom object.

The upshot of this is that we will use instances of NSDictionary to represent each record and store an individual's information. The table below shows which keys our dictionary will have, as well as some hypothetical data values:

Key Value
First Name Mike
Last Name Beam
email Address mikebeam@mail.utexas.edu
Home Phone (512) 123-4567

Note that the keys are all the same as the column identifiers we set above. This will prove to be most convenient and necessary when it comes to coding the data source side of Controller.

The second level of data organization is the whole collection of records. The way we're going to go this is with an array, specifically Cocoa's NSArray class. NSArray is conceptually no different than any other array you may have encountered in programming. An NSArray is just a collection of ordered, indexed elements. The only thing that is different is the interface for adding, inserting, deleting, and otherwise accessing members of an array collection.

Like dictionaries, Cocoa arrays give us the ability to transparently collect and organize any type of object we want. What I mean by transparently is that the interface for working with an array of text fields (we'll do this in a later column) is exactly the same as working with an array of strings, which is no different than working with an array of marines and siege tanks in some Starcraft-esque game. It might be overkill to suggest that dictionaries are valid array citizens too.

The plan then, is to collect all of our records into an array. Every time we create a dictionary, we'll store it in our array. Every time we need to get data out of a dictionary, we'll first retrieve the dictionary from the array.

Because most of our methods will use this array, we need to provide a global access point to it . This is done easily enough by declaring a new instance variable in Controller.h. We're using a mutable array (NSMutableArray) as opposed to a static array (NSArray) for the obvious reason that we want to be able to add, insert, or remove records at any point during runtime. NSMutableArray is just like NSArray, except you can change its contents after it's created.

Add the instance variable declaration for an NSMutableArray object named "records" to Controller.h in the same code block as where the outlets are declared:

NSMutableArray *records;

Before any of our operations can use the records array, we need to initialize it somehow when the application launches. The awakeFromNib method is a good place to do this. As an application is loading and unpacking its nib file, each object contained in the nib file is sent an awakeFromNib message (but not every class necessarily implements this message). We're going to set up Controller to initialize a new, empty records array when awakeFromNib is invoked.

Going back to Controller.m, add the following method

-(void)awakeFromNib
{
	records = [[NSMutableArray alloc] init];
}

Another way you can initialize this array is by overriding -(id)init. Both will serve to have everything ready when we need it. The first piece of our code is now in place, and we're ready to move onto implementing the action methods we declared in Interface Builder.

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

.

Problems

Unfortunately version 2 has a subtle, but serious flaw that results from the simple fact that it is not safe to modify a mutable array while it is being enumerated. Consider the situation of a table with five records in it, and you want to delete the first and last records (you can do discontinuous row selection by command-clicking). So we do [tableView selectedRowEnumerator] and in return we get an enumerator of NSNumber objects for 0 and 4.

The first time we send nextObject to the enumerator, we get returned the NSNumber "0", and we end up removing the first member of the mutable array records. Before this, our array had five records with indexes 0 through 4. Now our array has four records with indexes 0 through 3 (the remaining records all get shifted down one slot to fill the space previously taken by the record we just deleted).

The problem is that the next time through the loop, nextObject will return the other NSNumber in the enumerator, 4. We then try to remove the object at index 4 in records, but the previous removal of the object at index 0 ensured that there will be no object at index 4. The result of this is that we get an index out-of-range error, and the last record remains untouched. The moral of the story is to not change the contents of an array while enumerating it, and we have to adopt a more indirect approach to removing multiple records.

So we have to come up with a way to remove from records the selected rows after the enumeration has taken place. One thing we could do during the enumeration is to build an array of records we wish to remove, but not remove them until the enumeration has finished. We can then use the NSMutableArray method, removeObjectsInArray:, to remove from the receiver any objects contained in the array we created during the enumeration. Here we have version 3:

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

  while ( (index = [enumerator nextObject]) ) {
    tempObject = [records objectAtIndex:[index intValue]]; // No modification, no problem
    [tempArray addObject:tempObject]; // keep track of the record to delete in tempArray
  }

  [records removeObjectsInArray:tempArray]; // we're golden
  [tableView reloadData];
}

So we changed this by creating two variables: tempObject and tempArray. During the enumeration we did just what we said we would: retrieve the record from records corresponding to the selected row in each loop of the enumeration and store it in tempArray. After the enumeration finished, we have in tempArray all of the records from our data structure records that were selected for deletion in the table. With removeObjectsInArray, we remove from records all of the objects contained in tempArray (we're basically deleting the intersection of these two collections -- the common objects). Well! Fixed that problem!

Alert panels

Because deleting is not "undoable" in our implementation, it would probably be good idea to provide some sort of system feedback requesting user confirmation to a delete command so records aren't inadvertently deleted -- like with an alert dialog. The AppKit function, NSRunAlertPanel, allows us to do just that. This function takes the five arguments listed below.

If you want to format your message text a la printf(), you can do so by adding the variables to display in the message as optional arguments at the end of the argument list.

The return value of this function is an integer that indicates which button was pressed. AppKit defines several constants related to alert panels: NSAlertDefaultReturn, NSAlertAlternateReturn, NSAlertOtherReturn, and NSAlertErrorReturn, whose values are 0, 1, 2, and 3 respectively. The values of these constants are the same as the integer values returned by NSRunAlertPanel, and can thus be used to determine which button was pushed with an equality test.

Let's change our code to incorporate an alert panel. While we're at it, let's have the system beep at us when the panel opens using the AppKit function NSBeep(). Here is what our final, version 4 of the deleteRecord code looks like:

-(IBAction)deleteRecord:(id)sender
{
    int status;
    NSEnumerator *enumerator;
    NSNumber *index;
    NSMutableArray *tempArray = [NSMutableDictionary array];
    id tempObject;

    if ( [tableView numberOfSelectedRows] == 0 )
        return;

    NSBeep();
    status = NSRunAlertPanel(@"Warning!", @"Are you sure you want to delete the selected record(s)?", @"OK", @"Cancel", nil);

    if ( status == NSAlertDefaultReturn ) {
        enumerator = [tableView selectedRowEnumerator];

        while ( (index = [enumerator nextObject]) ) {
            tempObject = [records objectAtIndex:[index intValue]];
            [tempArray addObject:tempObject];
        }

        [records removeObjectsInArray:tempArray];
        [tableView reloadData];
    }
}

The first thing we did after declaring are variables was to use NSTableView's numberOfSelectedRows message to check how many rows are selected. If there are zero rows selected, then there's really no point in trying to delete nothing, and we return from the method.

Next we issued a beep with NSBeep() and opened up an alert panel -- the return value we store in the variable status. If you don't want one of the buttons to be displayed in the alert panel, use nil as the argument value for that button's text. We did this above to eliminate the third button from the alert panel. You can see what this alert panel looks like in the image below.

Screen shot.
The alert panel.

Then we compared status to the constant NSAlertDefaultReturn and executed our previously written record deletion code.

We've now set up a nice, simple system of working with the data, but we haven't yet talked about how the table actually gets the data from our array to the interface. The next section will focus on this.

Setting up the data source

Now is the time that we set up the data source. Recall from above that in Interface Builder we made a connection between Controller and tableView indicating that Controller would act as tableView's data source. Whenever a table view receives a reloadData message, that's its signal to go to send messages to the data source (Controller) to retrieve the data to display. What are the messages the table view object sends to the dataSource object to retrieve data? Before I can answer that question I need to say a word about Objective-C protocols.

We can find those messages in the NSTableDataSource protocol, which is a part of AppKit. In Objective-C we already know of class interfaces, which publicly publish all of the messages that can be sent to that class. But messaging is a two-way street.

Often it is useful to know what messages a class sends so we can prepare potential receivers of those messages to respond to them appropriately and intelligently. Protocols do just that. A protocol makes public knowledge any methods that an instance of a class sends. A class is said to conform to a protocol if it implements the methods laid out in the protocol.

In our situation, tableView is sending out messages to its data source expecting data back. The protocol NSTableDataSource tells us the what the messages are, including their arguments and what return value the tableView is expecting. For a dataSource object to function properly, it must implement the minimum required methods for it to be a dataSource and conform to the protocol.

If we look at the NSTableDataSource protocol reference, we'll find that there are two methods that we must implement for a table to display a data source's data. These two methods are numberOfRowsInTableView:, and tableView:objectValueForTableColumn:row:.

The first of these two methods is simple. Basically, this method is the table view's way of asking the data source "How many records do you have?" and the data source responds kindly with an integer. Add this method to your Controller implementation file with the following code:

- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
  return [records count];
}

The argument aTableView refers to the table view that is asking for the information. This argument allows for one dataSource object to manage data for multiple table views. In our application, we only have one, so we don't have to make use of that argument. If you had more than one table in your application, however, you might use this argument in a conditional statement: If aTableView is Table 1, then return the number of records in Array 1, or if aTableView is Table 2, then return the number of records in Array 2.

Following NSTableDataSource's recipe, we see that the next method to implement is tableView:objectValueForTableColumn:row:. The code for our implementation looks like this:

-(id)tableView:(NSTableView *)aTableView
  objectValueForTableColumn:(NSTableColumn *)aTableColumn 
  row:(int)rowIndex 
{
 id theRecord, theValue;
    
 theRecord = [records objectAtIndex:rowIndex];
 theValue = [theRecord objectForKey:[aTableColumn identifier]];

 return theValue;
}

Again, we're not going to use the aTableView variable from the arguments. All that interests us here is rowIndex and aTableColumn. Whenever we send tableView a reloadData message, the table view in turn scans through the columns and rows, sending this message to get the contents for each cell in the table. By attaching to the message row and column information, we're able to zero in on the correct data value within our two-dimensional data structure.

What we did first in our implementation was declare two generic object variables. By declaring these variables as type id, we eliminate the risk of trying to assign a variable to an object that it is not typed to. In the second line we assigned to theRecord the dictionary object we have stored at the rowIndex index of records.

Then, we use this dictionary to obtain the appropriate object to be displayed in aTableColumn. How do we know which is the right object? Well, fortunately we made the column identifiers and dictionary keys for each data field all the same, so all we have to do is send the dictionary object, theRecord, an objectForKey: message with the identifier string of the column as the key argument. This string is obtained by sending aTableColumn an identifier message.

For example, tableView might send the message to get data from the data source with row 0 and aTableColumn in the argument list. Our code will access the first element of records (corresponding to the first row of the table) and point the variable theRecord to that. We then get the column identifier string we set in Interface Builder from aTableColumn, which might be "First Name" and use that as a key to access the first name data contained in theRecord. The returned object from objectForKey: is then stored in the variable theValue which is then returned to tableView.

We finally have the primary pieces of code in place to make this fly, so compile it, hope there aren't any typos or anything, and run it. You should be able to add, insert, and delete records, and they should all appear in the table view.

As a final exercise, the NSTableDataSource protocol mentions a method that we can implement that allows us to change the data of a record directly in the table. If you double-click on a cell, you will be able to edit it, but the changes won't be saved unless this method is implemented in the dataSource object. Go ahead and give it a go.

And, the end

So that's how we implement a table in Cocoa. It seems like a lengthy process, but its really straightforward once you realize how the different pieces and objects interact. We had in our application the Controller object, which served the noble purpose of adding or subtracting records from our data structure in response to user actions, as well as letting the table know when it should update its contents.

We also had a simple data structure, which was nothing more than a collection of standard Cocoa mutable dictionaries penned up in a standard Cocoa mutable array. Finally, our table view had a dataSource outlet that told the table where to send messages to get data to display.

In our case, we made Controller our dataSource. The only requirement an object needed to work as a dataSource was that it minimally conform to the NSTableDataSource protocol. If you want to learn more about protocols in general, check it out in Object-Oriented Programming and The Objective-C Language (by Apple, located online for those of you who still haven't read this book). If you're having trouble, you can download the project folder for AddressBook.

This tiny application has quite a bit of functionality for the amount of work we did, but it's hardly complete. For one, we have no way to save the data between different sessions. Notice that we didn't create this application as a document-based application, which we certainly could have done, but I want to show you a different way we can save data (think about how you might save and load the data for this address book if we had gone the doc-based app route. Is there anyway we can make this transparent to the user so that the same set of data is opened at launch time by default, and saved each time we modify it?). So the plan for the next column is to implement data saving, as well as let our application remember other things about itself before running (such as window size). Until then, happy coding!

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.