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


Understanding the NSTableView Class

by Jose Cruz
08/08/2006

The table view widget is an interface feature commonly found in most modern software applications. It is used to display and edit data in a tabular format. The widget is modeled after the traditional accounting ledger, which displays financial data using ordered rows and columns. It improves upon the ledger concept, however, by allowing the displayed data to interact with the user as well as with each other.

This article provides an in-depth introduction to the use and implementation of the NSTableView Cocoa class. It demonstrates how to use Interface Builder to add an NSTableView widget to an application view. It shows how to implement a data-source controller that will maintain the data for either a single- or a multiple-column table view. It demonstrates how to allow users to edit the tabular data either directly on the table view or through a separate editing view. It also shows how to implement a custom formatter to display the data in a structured manner. Finally, it demonstrates how to customized certain table behaviors through the delegation process.

To assist in the understanding of the NSTableView class, the tutorial application, TableDemo, is made available for downloading. The application and its XCode project can be downloaded here. Readers are assumed to have a basic understanding of the Objective C language and the Cocoa Framework. They are also expected to have a working familiarity with the XCode development environment and its companion tool, Interface Builder.

The NSTableView Class

The NSTableView class is perhaps one of the most complex Cocoa class found in the Application Framework Kit. Its wide range of features can be quite daunting and formidable to the uninitiated. In fact, it is quite common to find questions on various Cocoa mailing lists pertaining to the proper implementation of the NSTableView class.

Figure 1. The NSTableView class.
Figure 1. The NSTableView class

Shown in Figure 1 is a simplified structure of the NSTableView class. The class itself consists primarily of an NSScrollView enclosing a number of subviews. It uses an NSTableHeaderView object to maintain the headers that denote each table column. It maintains a separate NSCell to be used for data editing. It also maintains an NSView to serve as the customizable upper-right cornerpiece. Finally, each table column subview is maintained as an NSMutableArray collection.

The NSTableView class accepts a wide range of data, each encapsulated using the appropriate NSObject. It then renders the data in human-readable form using either a custom NSFormatter object or, by default, the NSObject [description] message. The class also provides a number of useful features to manage its displayed data at runtime without the need for additional code. For instance, its table columns can be selected, resized, and reordered. It can display gridlines or alternating background colors to help delineate its tabular rows. Its built-in support for horizontal and vertical scrollbars can also be displayed either automatically or manually. It can also hide and show its column headers, as well as change the font and text displayed by its column headers.

The NSTableView class generates a number of messages to delegate control over certain display behaviors to a target object. The object can then determine which table column is being selected, moved, or resized. It can also control which table row or column will be selected, or which table cell will be edited. The controller object can also use the delegate messages to determine how the table view will respond to certain mouse events.

Unfortunately, the NSTableView class does not automatically enable support the Edit menu. Because of this limitation, additional code must be provided to enable basic menu actions such as Copy, Select All, and Select None. Hopefully, Apple will address this in future implementations of the class by providing delegate methods that can be used to add the necessary menu handling code.

The Interface Builder application makes it quite easy to add an NSTableView object to an application view. Simply click on the Cocoa Data Views icon on the Palette window, and drag and drop the widget onto the desired view, which can either an NSWindow, NSPanel, or NSView (Figure 2). To configure its default display attributes, click to select the widget and choose Show Inspector from the Tools menu. Then use the Inspector Panel (Figure 3) to set the desired display attributes.

Figure 2. The Cocoa Data Views palette.
Figure 2. The Cocoa Data Views palette

Figure 3. The NSTableView Inspector panel.
Figure 3. The NSTableView Inspector panel

When an NSTableView widget is added to the application view, Interface Builder automatically instantiates an NSTableView object and adds the object to the responder chain of the parent view. It also populates the table widget with placeholder test data. This can be switched off by clearing the checkbox labeled Populate NSTableView/NSOutlineView (Cocoa only) in the Preferences window of Interface Builder.

Currently, Apple does not provide any official OS X guidelines for the look and feel of table views. Nonetheless, there are a number of implicit rules to create a consistent and legible implementation of an NSTableView object.

The Data-Source Process

Some table view widgets allow data to be directly added or remove from the view. These widgets usually requires the tabular data to be converted first to a predetermined form. One such example is the REALbasic ListBox control, which requires its tabular data to be converted first into strings.

Other table view widgets require the entire tabular data encapsulated in a separate object. The widgets are then initialized using the encapsulating object. An example of such a widget is the Java JList class. It requires its data to be encapsulate using a DefaultListModel object. Then, an instance of the JList object is created by passing the DefaultListModel object into the class constructor.

Both aforementioned widgets maintain their tabular data internally, which adds to their resource overhead. The NSTableView class, on the other hand, uses a target-action approach called data-sourcing to manage its tabular data. Each time the table updates its display, it sends its requests for data to a separate object known as a data-source controller. Often, this controller is derived from the NSObject class and uses a collection object (such as an NSArray or NSDictionary) to serve as its internal data buffer.

To function correctly, a data-source controller must support at least two of the basic protocol methods provided by the NSTableView class. The first protocol method:

     - (int)numberOfRowsInTableView:(NSTableView *)aTable;

returns the total number of data items available from the controller to the NSTableView parameter, aTable. If the controller returns an integer value less than 0, aTable will interpreted this as a zero-data indicator.

The second protocol method:

     - (id)tableView:(NSTableView *)aTable objectValueForColumn:(NSTableColumn *)aCol row:(int)aRow;

provides the controller the references to the table cell being updated. The controller then retrieves and returns the data encapsulated in an appropriate NSObject to be displayed in the specified cell.

Figure 3 is a UML sequence diagram illustrating a typical data-source process. In this diagram, aTable is assume to contain only single column. The data-source controller, aDatSrc, uses an NSArray object to serve as its internal data buffer, aBuffer.

Figure 4. The Data-Source Process.
Figure 4. The Data-Source Process

Each time aTable receives a [reloadData] message, it first queries the number of rows to be displayed by sending a [numberOfRowsInTableView:] message to aDatSrc. aDatSrc responds by first sending a count message to aBuffer and then sends the resulting count value back to aTable as an unsigned integer.

Once the number of rows has been established, aTable then starts querying for data to be displayed in each row and column by sending a [tableView:objectValueForTableColumn:row:] message to aDatSrc. The row parameter contains the index of the row that is visible on aTable whereas the objectValueForTableColumn parameter contains a reference to the NSTableColumn subview being updated. aDatSrc then queries aBuffer for the desired data by sending it an objectAtIndex message passing along the row index provided by aTable.

If aTable is a multi-column table, aDatSrc first determines which column subview is being updated by sending an [identifier] message to the objectForTableColumn parameter, aCol. aCol responds by returning its unique identifier, which is usually interpreted as an NSString object.

Notice that both data-source methods pass references to the NSTableView widget through the aTable parameter. This allows the same data-source controller to service multiple NSTableView widgets. To identify which widget is being updated, send a [tag] message to aTable. It then returns an NSNumber containing the unique identifier assigned to the widget through the NSTableView Inspector panel (Figure 3).

Notice as well that both the row index and the total row count are represented as signed integers. This implies that NSTableView is limited to a maximum of 2 147 483 648 rows of data at a time. On the other hand, collection classes such as >NSArray represent their indices and item count as unsigned values. This implies that a collection class can contain up to 2256 possible data items. Nonetheless, these discrepancies in storage size is not a major factor as NSTableView mostly updates those rows that are immediately visible to the user.

The easiest way to create a data-source controller and bind it to the desired NSTableView widget is to use Interface Builder. Navigate to the Classes panel of the application nib window and create a subclass of NSObject. Make sure to rename the subclass appropriately. To bind the controller to the widget, first instantiate the controller by selecting its name from the Classes panel and choose Instantiate from the Classes menu. Then in the Instances panel, double-click to select the NSTableView widget and <CTRL>-drag a connection line from the widget to the instantiated controller (Figure 4). This will display an Inspector panel with the dataSource entry automatically selected. Click on the Connect button to complete the binding.

Other classes such as NSTimer or NSThread can also be subclassed and used as the basis for the data-source controller. Be aware, however, that Interface Builder displays classes mostly from the Application Kit framework. If the controller is based on a class from the Foundation Kit or from a third-party framework, the subclassing and binding process will have to be done manually. To manually bind aDatSrc to aTable, send a [setDataSource] message to the table view. Make sure to perform the binding at least during the awakeFromNib message event of aTable.

      [aTable setDataSource:aDatSrc];

Figure 5. Binding a data-source controller to the table view.
Figure 5. Binding a data-source controller to the table view

Once the controller has been bound to the widget, choose Create Files from the Classes menu and follow the instructions provided by the ensuing dialog. Interface Builder then generates the source and header files, and adds them to the XCode project where the controller internals can be defined.

The number of visible columns on the NSTableView widget determines the collection class suitable for use as the internal buffer of the data-source controller. For instance, if the widget only has a single table column, an NSArray class can serve as the internal buffer. Since both objects uses the same zero-indexing scheme, data transfers between the two is straightforward and synchronized. Furthermore, the data-source protocol methods can be implemented as shown in Listing 1.

Listing 1. The data-source controller for a single-column table view.

aDatSrc.h
       #import <Cocoa/Cocoa.h>
       
       @interface aDatSrc : NSObject
       {
             @private
             NSArray     *aBuffer;
       }
       @end
aDatSrc.m
       #import "aDatSrc.h"
       
       @implementation aDatSrc
       // The data-source protocol methods
       - (int)numberOfRowsInTableView:(NSTableView *)aTable
       {
             return ([aBuffer count]);
       }
       
       - (id)tableView:(NSTableView *)aTable 
             objectValueForColumn:(NSTableColumn *)aCol 
                                       row:(int)aRow
       {
             return ([aBuffer objectAtIndex:aRow]);
       }
       
       // -- additional code to initialize, access, and 
       // -- modify the data buffer
       // -- ...
      @end

On the other hand, if the widget has multiple table columns, an NSDictionary class would be more suitable. Data for each column subview is then stored into an NSArray. Then each NSArray object is stored into the NSDictionary object as a key/value entry using the unique identifier assigned to each NSTableColumn subview as the dictionary key (Figure 5). Listing 2 shows how the data-source protocol methods can be implemented for a multiple-column table view.

Figure 6. Proposed data buffer for a multi-column table display.
Figure 6. Proposed data buffer for a multi-column table display

Listing 2. The data-source controller for a multiple-column table view.

aDatSrc.h
	     #import <Cocoa/Cocoa.h>
      
      @interface aDatSrc : NSObject
      {
           @private
           NSDictionary     *aBuffer;
      }
      @end
aDatSrc.m
	     #import "aDatSrc.h"
      
      @implementation aDatSrc
      // The data-source protocol methods
      - (int)numberOfRowsInTableView:(NSTableView *)aTable
      {
           return ([aBuffer count]);
      }
      
      - (id)tableView:(NSTableView *)aTable 
                objectValueForColumn:(NSTableColumn *)aCol 
                  row:(int)aRow
      {
           id     loc_id, loc_dat;
           NSArray          *loc_col;
           
           loc_id = [aCol identifier];
           if (loc_id != nil)
           {
                loc_col = [aBuffer objectForKey:loc_id];
                if (loc_col != nil)
                {
                     loc_dat = [loc_col objectAtIndex:aRow];
                }
           }
           return (loc_dat);
      }
      
      // -- additional code to initialize, access, and 
      // -- modify the data buffer
      // -- ...
      @end

Notice that both controller implementations declare their internal buffers as a private property. This prevents the buffer from being directly accessed and modified by external objects. As part of a good object-oriented design, buffer access must be done only through the requisite accessor and modifier methods.

Notice as well that the collection classes used to implement the internal buffer is immutable. Mutable collection classes such as NSMutableArray and NSMutableDictionary should be used only when data editing is to be supported. Otherwise, use immutable classes to minimize the resource overhead and memory requirements of the application.

The Data Editing Process

The NSTableView class also allows its tabular data to be edited while being displayed. Furthermore, the displayed data can be edited either directly on the table view itself (inline editing), or indirectly through a separate view (panel editing). Choosing the appropriate technique depends largely on the amount and type of data being edited. However, the data-source controller must derive its internal buffer from a mutable collection class, such as an NSMutableArray, if it is to support editing.

Inline Editing is most suitable when the editable data fits the confines of its enclosing view. One notable example is a spreadsheet document whose alphanumeric data are edited directly within their respective cells. The edit process itself is usually initiated by either a click or double-click action administered to the desired cell. Since the entire process occurs directly on the table widget, minimal resources are required to track and display the resulting changes.

The NSTableView class supports inline editing through a special data-source protocol message,

      - (void)tableView:(NSTableView *)aTable setObjectValue:(id)aData 
                    forTableColumn:(NSTableColumn *)aCol row:(int)aRow

This message is sent by the view to its data-source controller whenever a user double-clicks on the data to be edited. It passes the following four parameters to the controller:

Figure 6 shows the signal flow of a typical inline editing session on a single-column table view. When the user double-clicks on a table cell, aTable sends the [tableView:setObjectValue:forTableColumn:row:] message to the data-source controller, aDatSrc. The controller updates its internal buffer, aBuffer, by sending it a [replaceObjectAtIndex:withObject:] message. Then, once the buffer is updated, aTable updates its display by sending itself a [reloadData] message, which starts a new data-source process.

A multiple-column table view also follows the same signal flow for its inline editing session. The only difference is that aDatSrc must now determine which column subview is being updated by sending an [identifier] message to the forTableColumn parameter, aCol. It then sends an [objectForKey:] to aBuffer, assuming the buffer is based on NSMutableDictionary. The buffer responds by returning a reference to the NSMutableArray object that corresponds to the specified column. Finally, aDatSrc updates the appropriate entry by sending a [replaceObjectAtIndex:withObject:] message with the edited data to the mutable array.

Figure 7. A typical inline editing process
Figure 7. A typical inline editing process

Listing 3 shows how inline-editing could be implemented in the data-source controller. Notice that the edited value is first compared against the buffered value before it is committed to the buffer. Interestingly enough, just double-clicking on a table cell and then moving the selection focus elsewhere also causes aTable to send a [tableView:setObjectValue:forTableColumn:row:] message to aDatSrc. This is a wasted message as the previously selected data may have remained unchanged. Verifying if an actual change did occurred helps minimize the resource overhead involved with buffer access.

Listing 3. Implementing an inline-editing process.

 - (void)tableView:(NSTableView *)aTable setObjectValue:(id)aData 
   forTableColumn:(NSTableColumn *)aCol 
			row:(int)aRow
{
     id loc_id, loc_data;
      NSString     *loc_log;
      
      // identify the table column
      loc_id = [aCol identifier];
      if ([loc_id isKindOfClass:[NSString class]])
      {
           // determine the old cell value
           loc_data = [[self testBuffer] objectForKey:loc_id];
           loc_data = [loc_data objectAtIndex:aRow];
           
           // compare the old cell value against the "new" value
           if (![loc_data isEqual:aData])
           {
                // update the data buffer
                [[[self testBuffer] objectForKey:loc_id]
                replaceObjectAtIndex:aRow withObject:aData];
           }
      }
}

Also, to enable inline editing for a specific column subview, send a [setEditable:] message with a BOOL argument of YES to that subview. The message tells the subview to interpret any double-action signal as the start of the inline editing session. For instance, to enable inline editing for the column subview, editMe, of aTable, send the message as follows:

      [[aTable tableColumnWithIdentifier:@"editMe"] setEditable:YES];

to that subview. Here, the [tableColumnWithIdentifier:] message is used to reference the column subview with the unique NSString identifier @"editMe". To determine if the same subview is editable, send an [isEditable] message as follows:

      BOOL aFlag;
      //..
      aFlag = [[aTable tableColumnWithIdentifier:@"editMe"] isEditable];

Inline editing is restricted to the amount of visible space provided by the table cell. If the edited information consists of a longer phrase, portions of it will be obscured from view. Also, inline editing is unsuitable in situations where the edited data is part of a larger set. For instance, editing customer information may mean editing the customer's name, which is visible, as well as the address, which is not. For these scenarios, panel editing is the more suitable approach.

Panel Editing uses a separate view to contain the relevant table information for editing. Once editing is done, a user action is required to confirm and commit the changes back into the buffer. Panel editing always require additional resources to display and track the changes made to the data. This is especially true when multiple data items are being edited at one time.

Figure 8 shows the signal flow of a typical panel-editing process for a single column table view. The controller, aDatEdit, is assigned as the action target of aTable using the [setTarget:] message.

[aTable setTarget:aDatEdit];

Also, all double-action signals generated by aTable are then routed to the doubleAction method of aDatEdit using the [setDoubleAction:] message. When a user double-clicks on a table cell, aTable directly invokes the doubleAction method of aDatSrc. Since the [setDoubleAction:] message takes a SEL argument, the doubleAction method needs to be recast using the @selector directive.

[aTable setDoubleAction:@selector(doubleAction)];

When aDatEdit receives the double-action signal, it displays its editing panel, aPanel, which is subclassed from NSPanel in this example. It then determines the currently selected row by sending a [selectedRow] message to aTable (not shown). It sends the row index to aDatSrc, which returns the data corresponding to that row. aDatEdit then populates its editing panel with the received table data.

Once the user finished changing the data and has decided to commit those changes, aDatEdit disposes of aPanel and sends the edited data back to aDatSrc for storage into its internal buffer. aDatSrc then sends a [reloadData] message to aTable, thus starting a new data-source process. For another example of the panel editing process, examine the DemoEdit.m source file of the TableDemo application.

Figure 8. A typical panel editing process.
Figure 8. A typical panel-editing process

In order for panel editing to work, make sure to disable inline editing for the column subview that will implement a panel editing session. If left enabled, the double-action signal will be intercepted instead by that subview and, as a result, the method assigned using the [setDoubleAction:] message will not be invoked. For example, to disable inline editing on the column subview, @"panelEdit", of aTable, send a [setEditable:] message with a BOOL value of NO to that subview.


      [[aTable tableColumnWithIdentifier:@"panelEdit"] setEditable:NO];
 

Since the editable states of each column subview can be individually set, NSTableView can support a mixture of inline and panel editing sessions. The type of data displayed on each column will then dictate the appropriate session for that column. For instance, a double-click action on a table column displaying the customer's surname starts an inline editing session whereas the same action on the column displaying the customer's address starts a panel editing session.

The Data Formatting Process

Whenever NSTableView displays its tabular data, it sends a [description] message to each data object. This converts the data into a human-readable form that is non-localized and unformatted by default. The converted data is then displayed in the appropriate column subview of NSTableView.

However, if NSTableView is to display its data in a specific format, an NSFormatter object must be assigned to the appropriate column subview. To do so using Interface Builder, first switch to the Text panel of the Cocoa palette, and drag and drop one of two NSFormatter objects from the palette and onto the desired subview (Figure 8). Select the formatter with a dollar symbol to instantiate an NSNumberFormatter. Otherwise, select the one with the calendar symbol to instantiate an NSDateFormatter. Make sure that correct column subview is highlighted before releasing the mouse button and thus binding the formatter to that subview.

Figure 9. Attaching an NSFormatter object in Interface Builder.
Figure 9. Attaching an NSFormatter object in Interface Builder.

Once the appropriate formatter has been attached to the column subview, click to select the subview and choose Show Inspector from the Tools menu. Interface Builder then displays the configuration panel for the attached formatter (Figure 9). Enter the desired format strings into the fields provided by the panel. For more information on how to create a custom format string, consult one of the two Apple references at the end of this article.

Figure 10. Configuring the NSFormatter object in Interface Builder.
Figure 10. Configuring the NSFormatter object in Interface Builder.

More sophisticated data formats can also be implemented by manually subclassing the NSFormatter class and binding the instance of the subclass to the table view widget. This approach provides a degree of control that is not possible with neither NSDateFormatter nor NSNumberFormatter. For instance, a custom formatter can attach an appropriate unit of measure to the formatted data. It can also be used to normalize the data with respect to a specific reference value.

To function correctly, the NSFormatter subclass must override the following inherited methods with the appropriate formatting code.

- (NSString *)stringForObjectValue:(id)anObj
Format the data contained in anObj to the appropriate human-readable representation.
Returns the formatted data as an NSString.
- (BOOL)getObjectValue:(id *)anObj forString:(NSString *)aStr errorDescription:(NSString **)anErr
Restore the NSString representation, aStr, back into anObj.
Preferably, the class type of anObj is the same one used in the [stringForObjectValue:] message.

Like the data-source protocol methods, overriding the [getObjectValue:forString:errorDescription:] method is necessary only if the table view is to allow inline editing. Once the formatting code has been implemented, bind the custom formatter object to the appropriate column subview by using the [setFormatter:] message. For example, to bind the formatter object, aFormat, to the column subview, @"myCol", of aTable:

     [[aTable tableColumnWithIdentifier:@"myCol"] setFormatter:aFormat];

Listing 4 is a example of a custom NSFormatter object that displays the incoming numerical data in terms of kilobytes. The [stringForObjectValue:] method takes the NSNumber parameter, aArg, and divides its numerical value by 1024. It then returns the converted result as a formatted numerical string with a 3-decimal place precision. However, if aArg is not an NSNumber, the method throws an NSInvalidArgumentException error.

The [getObjectValue:forString:errorDescription:] method receives the table cell data as an NSString. It converts the string to its numerical value and multiplies the value by 1024. It encapsulates the product result into an NSNumber and updates the parameter, anObj with the object. The method returns a YES to indicate a successful conversion; otherwise, it returns a NO.

Listing 4. A custom formatter that converts between bytes and kilobytes.

     Num2Hex.m
     
     @implementation Num2Hex
     - (NSString *)stringForObjectValue:(id)aArg
     {
          NSException  *loc_err;
          NSString     *loc_val;
          double       loc_dbl;
          
          // parameter check
          if (aArg != nil)
          {
               // identify the type of object to be formatted
               if ([aArg isKindOfClass:[NSNumber class]])
               {
                    // reformat the data
                    loc_dbl = [aArg doubleValue];
                    loc_dbl = loc_dbl / 1024.0;
                    
                    // convert the data to a string
                    loc_val = [NSString stringWithFormat:@"%5.3f",
                         loc_dbl];
               }
               else
               {
                    // raise an exception
                    loc_err = 
                         [NSException exceptionWithName:NSInvalidArgumentException 
                              reason:@"Unsupported datatype"
                              userInfo:nil];
                    [loc_err raise];
               }
          }
          else
          {
               // raise an exception
               loc_err = 
                    [NSException exceptionWithName:NSInvalidArgumentException 
                         reason:@"Nil argument"
                         userInfo:nil];
               [loc_err raise];
          }
          // return the formatting results
          return (loc_val);
     }
     
     - (BOOL)getObjectValue:(id *)anObj forString:(NSString *)aStr 
           errorDescription:(NSString **)anErr
     {
          BOOL          loc_chk;
          double        loc_dbl;
          
          // parameter check
          loc_chk = (aStr != nil);
          if (loc_chk)
          {
               // perform the conversion
               loc_dbl = [aStr doubleValue];
               loc_dbl = loc_dbl * 1024.0;
               *anObj = [NSNumber numberWithDouble:loc_dbl];
               loc_chk = (*anObj != nil);
          }
          // return the conversion results
          return (loc_chk);
     }
     

Miscellaneous Display Attributes

Formatting the Tabular Data

Another customizable aspect of the NSTableView display process is in data formatting. Whenever it receives a data object for a specific row and column, the NSTableView renders the data in human-readable form by sending a description message to the object. The object then returns a non-localized and unformatted textual description of itself as an NSString.

You can assign an NSFormatter object to the NSTableView data cell in order to display its data in a specific format. One way to accomplish this is to use Interface Builder to drag-and-drop one of two NSFormatter objects from the Cocoa palette onto a specific NSTableColumn. You can then configure the NSFormatter object through the Inspector window, as shown in Figure 14. Choose Show Inspector from the Tools menu to display the Inspector window.

Using Interface Builder to assign an NSFormatter to your NSTableView object is sufficient if all you need is the functionality built into the NSNumberFormatter and NSDateFormatter objects, which is what the palette provides. However, for more sophisticated formatting needs, you will need to subclass the NSFormatter class and override the following two methods:

- (NSString *)stringForObjectValue:(id)anObj
Convert the data contained in anObj to the appropriate human-readable representation as an NSString.
- (BOOL)getObjectValue:(id *)anObj forString:(NSString *)aStr errorDescription:(NSString **)anErr
Restore the NSString representation into the appropriate anObj data.
Preferably, the class type used by anObj is the same one used in the stringForObjectValue: message.
This method is only required if the NSTableView object is displaying editable data.

Creating a custom NSFormatter object allows you to control the data format to a degree not possible with either an NSNumberFormatter or an NSDateFormatter object. For example, your custom NSFormatter can take an NSNumber object and convert its numerical value as a hexadecimal string. Another example is to have your NSFormatter take a file size (in bytes) and convert it to kilobytes (Listing 4).

Listing 4. An NSFormatter class that converts between bytes and kilobytes.

	DemoFormat.m
	
	@implementation DemoFormat
	- (NSString *)stringForObjectValue:(id)aArg
	{
		NSException	*loc_err;
		NSString	*loc_val;
		double		loc_dbl;
		
		// parameter check
		if (aArg != nil)
		{
			// identify the type of object to be formatted
			if ([aArg isKindOfClass:[NSNumber class]])
			{
				// reformat the data
				loc_dbl = [aArg doubleValue];
				loc_dbl = loc_dbl / 1024.0;
				
				// convert the data to a string
				loc_val = [NSString stringWithFormat:@"%5.3f",
					loc_dbl];
			}
			else
			{
				// raise an exception
				loc_err = 
					[NSException exceptionWithName:NSInvalidArgumentException 
						reason:@"Unsupported datatype"
						userInfo:nil];
				[loc_err raise];
			}
		}
		else
		{
			// raise an exception
			loc_err = 
				[NSException exceptionWithName:NSInvalidArgumentException 
					reason:@"Nil argument"
					userInfo:nil];
			[loc_err raise];
		}
		// return the formatting results
		return (loc_val);
	}
	
	- (BOOL)getObjectValue:(id *)anObj forString:(NSString *)aStr 
		  errorDescription:(NSString **)anErr
	{
		BOOL		loc_chk;
		double		loc_dbl;
		
		// parameter check
		loc_chk = (aStr != nil);
		if (loc_chk)
		{
			// perform the conversion
			loc_dbl = [aStr doubleValue];
			loc_dbl = loc_dbl * 1024.0;
			*anObj = [NSNumber numberWithDouble:loc_dbl];
			loc_chk = (*anObj != nil);
		}
		// return the conversion results
		return (loc_chk);
	}

The Table Delegation Process

The NSTableView class uses delegation to provide control over some of its events. The generated messages are often sent to the controller object designated as the delegate target for the table view. Some messages are used to track changes in a table event. Others are used to validate a specific event by responding with a YES or NO value. Not all delegate messages, however, are supported in versions of MacOS X older than 10.4. To ensure backward compatibility, use the following #if...#endif directive block to encapsulate the newer messages.

	#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
		// OS X 10.4 or newer code goes here
	#else
		// OS X 10.3 or earlier code goes here
	#endif

Interface Builder makes it easy to bind a delegate target to the table view. Simply follow the same directions for binding a data-source target to the table view; except this time, choose the delegate option from the Inspector panel (Figure 10). The delegate target can also be bound manually by sending a [setDelegate:] message to the table view. For example, sending [aTable setDelegate:aDelegate]; binds the controller, aDelegate, as the delegate target of aTable. Note that multiple table views can use the same delegate target. It is up to the target to determine which view generated the message. In some of the following code examples, this is done by sending a [tag] message to each aTable parameter. aTable responds by returning an NSNumber containing the unique tag value assigned to the table view through the Inspector panel (Figure 3).

Figure 11. Binding a delegate target to the table view.
Figure 11. Binding a delegate target to the table view.

To best demonstrate their functionality, most of the NSTableView delegate messages are categorized in terms of five table view events. Also, code fragments for each delegate message are provided in this article. For detailed descriptions about each NSTableView delegate messages, consult one the official Apple documents listed at the end of this article. Also review the TableDemo source file, DemoDelegate.m, for additional code examples for each message.

Figure 11 is the sequence diagram of two table resize events. In the first scenario, the user first resizes the NSWindow view, aWindow, either by dragging its grow icon or by choosing Zoom from the Window menu. If aDelegate is assigned to be the delegate target, aWindow sends a [windowWillResize:toSize:] message informing the delegate of the change in window size. Note that this delegate message is generated only by NSWindow. Neither NSView nor NSPanel generates the aforementioned message.

Afterwards, aWindow passes the resize event to its NSTableView object, aTable, which then checks if its columnAutoResizingStyle flag is not set to NSTableViewNoColumnResizing. If it is not, aTable sends a [tableViewColumnDidResize:] message to aDelegate. This informs aDelegate the table column subviews that have been resized. The order in which each subview is resized depends largely on the setting used by columnAutoresizingStyle flag of aTable.

In the second resize scenario, the user manually resizes a table column by placing the cursor over a column header border. If the NSTableView flag, allowColumnResizing, is set to YES, aTable changes the cursor image from an arrow icon to a drag icon. It then sends a [tableView:mouseDownInHeaderOfTableColumn:] message to aDelegate after a mouse-down signal. The message informs aDelegate which column subview is located on the lefthand side of the cursor during the resize event.

On the next mouse-up signal, aTable sends two [tableViewColumnDidResize:] messages to aDelegate. The first message is for the column subview that was resized, while the second is for the subview affected by the resize. However, if the user only resized the rightmost subview, aTable sends only a single [tableViewColumnDidResize:] message for that subview.

Figure 12. Table column resize event.
Figure 12. Table column resize event.

The [tableView:mouseDownInHeaderOfTableColumn:] message (Listing 5) determines which column header has been clicked on by the user. It provides two input parameters: aTable, the table view that generated the message; and aCol, the column subview that owns the header. After identifying which table generated the message, the delegate sends an [identifier] message to aCol thus determining which column subview was selected by the user. Notice that both aCol and aTable use a different NSObject class for their respective unique identifiers.

Listing 5. Tracking a column header selection.


- (void)tableView:(NSTableView *)aTable mouseDownInHeaderOfTableColumn:
     (NSTableColumn *)aCol
{
     NSNumber      *id_table;
     NSString      *id_column;
     
     // parameter check
     if ((aCol != nil) && (aTable != nil))
     {
          // identify which table and column generated the message
          id_table = [aTable tag];
          id_column = [aCol identifier];
          
          // handle the delegate message
          // ...
     }
}

The [tableViewColumnDidResize:] message (Listing 6) determines which column subview has been resized. It provides a single input parameter, aSig, which is an NSNotification object containing additional details about the message. To determine which table view generated the message, an [object] message is first sent to aSig to retrieve the NSTableView reference. Then, a [tag] message is send to the reference to retrieve the table's unique identifier.

To determine which column subview has been resized, a [userInfo] message is first sent to aSig. This returns an NSDictionary object containing additional information about the notification signal. Then, an [objectForKey:] message is sent to the dictionary with a key value of @"NSTableColumn". The dictionary responds by returning an NSTableColumn reference. Finally, an [identifier] message is sent to the reference thus retrieving the subview's unique identifier.

Listing 6. Tracking a table column resize.


- (void)tableViewColumnDidResize:(NSNotification *)aSig
{
     NSNumber          *id_table;
     NSString          *id_column;
     NSTableColumn     *theColumn;
     
     // parameter check
     if (aSig != nil)
     {
          if ([[aSig object] isKindOfClass:[NSTableView class]])
          {
               // determine which table generated the message
               id_table = [[aSig object] tag];
               
               // determine which column has been resized
               theColumn = [[aSig userInfo] objectForKey:@"NSTableColumn"];
               id_column = [ theColumn identifier];
               
               // handle the delegate message
               // ..
          }
     }
}

The second table event is a column reorder event, whose sequence diagram is shown in Figure 12. This event occurs when the user drags a column subview to a different location on the table. Each time a user selects a column subview by clicking on its header, aTable sends a [tableView:mouseDownInHeaderOfTableColumn:] message to aDelegate. Then, the moment the user starts dragging, aTable checks the status of its allowsColumnReordering flag. If the flag is set to YES, aTable displays a translucent image of the column subview being dragged. Also, if the dragged image moves at least halfway across another column subview, aTable repositions that subview in the opposite direction of the drag movement.

Once the user finishes the drag, thus generating a mouse-up signal, aTable checks to see if the column subview has indeed been moved. If so, it sends a [tableViewColumnDidMove:] message to aDelegate. It also sends a [tableView:didDragTableColumn:] message signifying the end of the reorder event. However, if the column subview did not move, only the [tableView:didDragTableColumn:] message is sent to aDelegate.

Figure 13. Table column reorder event.
Figure 13. Table column reorder event.

Two new delegate messages are generated during the column reorder event. The first one, [tableViewColumnDidMove:], (Listing 7) determines the old and new locations of the column subview. Like [tableViewColumnDidResize:], this message provides a single NSNotification parameter, aSig. The same code used in Listing 6 to determine the table view that generated the message as well as the column subview that was moved is also applicable here. Additionally, the old and new locations of the column subview is determined by also sending an [objectForKey:] message to the userInfo dictionary. To determine the old column index, send the message with a key value of @"NSOldColumn". To determine the new column index, send the same message but with a key value of @"NSNewColumn". Either one will return an NSNumber containing the specified index.

Listing 7. Determining the change in column indices.


- (void)tableViewColumnDidMove:(NSNotification *)aSig
{
     NSNumber       *id_table, col_old, col_new;
     NSString       *id_column;
     NSTableColumn  *theColumn;
     
     // parameter check
     if (aSig != nil)
     {
          if ([[aSig object] isKindOfClass:[NSTableView class]])
          {
               // determine which table generated the message
               id_table = [[aSig object] tag];
               
               // determine which column has been resized
               theColumn = [[aSig userInfo] objectForKey:@"NSTableColumn"];
               id_column = [ theColumn identifier];
               
               // determine the old and new column indices
               col_old = [[aSig userInfo] objectForKey:@"NSOldColumn"];
               col_new = [[aSig userInfo] objectForKey:@"NSOldColumn"];
               
               // handle the delegate message
               // ..
          }
     }
}

The second message, [tableView:didDragTableColumn:], determines which column subview has been moved by the user. As shown in Figure 12, this message is sent whether or not the column subview was moved to a new location. The message shares the same input parameters as [tableView:mouseDownInHeaderOfTableColumn:]. Because of this, the code fragment shown in Listing 5 can be used to handle this message with minimal modifications.

The next possible table event is the column selection event; its sequence diagram is shown in Figure 13. Like in the previous events, aTable first sends a [tableView:mouseDownInHeaderOfTableColumn:] message to aDelegate indicating which column header has been clicked. Then, on the next mouse-up signal within the same header, aTable checks the status of its allowsColumnSelection flag. If it is set to YES, aTable sends at most four additional messages to aDelegate in the following order:

  1. a [selectionShouldChangeInTableView:] message asking if the table selection focus should be allowed to change. aDelegate responds by returning a YES to allow the change; otherwise, it returns a NO.
  2. a [tableView:shouldSelectTableColumn:] message asking if the table column should be selected. Again, aDelegate responds with either a YES or NO. This is sent only after aDelegate has responded with a YES to the [selectionShouldChangeInTableView:] message.
  3. a [tableViewSelectionIsChanging:] message indicating that the table selection focus is about to change. Again, it is only sent after aDelegate responded with a YES to the [tableView:shouldSelectTableColumn:] message.
  4. a [tableViewSelectionDidChanged:] message indicating that the table selection focus has successfully changed.

At the end of the sequence, aTable sends a [tableView:didClickedTableColumn:] message to aDelegate indicating the end of the selection event. Like its predecessor, the [tableView:mouseDownInHeaderOfTableColumn:] message, it informs aDelegate that a column header has been clicked on by the user. However, whereas the predecessor is sent after a mouse-down signal, the [tableView:didClickedTableColumn:] message is sent after a mouse-up signal. It is also sent regardless of whether or not all of the four messages listed previously were generated and sent by aTable.

Figure 14. Table column selection event.
Figure 14. Table column selection event.

Five additional delegate messages are generated during the column selection event. The first message, [selectionShouldChangeInTableView:] (Listing 8), asks the delegate if the change in selection should be allowed. It provides a single input parameter, aTable, indicating the source of the message. The delegate determines the table row and column being selected by respectively sending a [selectedRow] and a [selectedColumn] message to aTable. aTable responds by returning the indices corresponding to the selected row and column. In the case of a column selection event, however, aTable always returns a 0 for the row and a -1 for the column. Either way, the delegate validates the change request and returns the appropriate BOOL value.

Listing 8. Verifying the change in table selection focus.


- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTable
{
     BOOL      loc_chk;
     int       loc_row, loc_col;
     
     // validate the input argument
     loc_chk = (aTable != nil);
     if (loc_chk)
     {
          // retrieve the selected table indices
          loc_row = [aTable selectedRow];
          loc_col = [aTable selectedColumn];
          
          // verify the delegate message
          //...
     }
     // return the verification results
     return (loc_chk);
}

The second message, [tableView:shouldSelectTableColumn:] (Listing 9), asks the delegate if the specified table column should be selected. It provides the same input parameters as the [tableView:mouseDownInHeaderOfTableColumn:] message. The delegate also uses the same approach shown in Listing 5 to determine the source of the message and the column subview being selected. It then verifies the data collection associated with the subview and returns the appropriate BOOL value.

Listing 9. Verifying the table column selection.


- (BOOL)tableView:(NSTableView *)aTable shouldSelectTableColumn:(NSTableColumn *)aCol
{
     NSNumber      *id_table;
     NSString      *id_column;
     BOOL           loc_chk;
      
      // parameter check
      if ((aCol != nil) && (aTable != nil))
      {
            // identify which table and column generated the message
            id_table = [aTable tag];
            id_column = [aCol identifier];
            
            // verify the delegate message
            // ...
      }
     // return the verification results
     return (loc_chk);
}

The next two messages, [tableViewSelectionIsChanging:] and [tableViewSelectionDidChanged:], inform the delegate about the change in selection focus. The first one tells the delegate that the selection focus is about to change whereas the second one tells that the focus has successfully changed. Both messages uses the same input parameter, aSig, as [tableViewColumnDidResize:]. Because of this similarity, the same code fragment shown in Listing 6 can be used to handle the two messages with minimal modification.

The fifth and final message generated in the column selection event is [tableView:didClickedTableColumn:]. This message tells the delegate which column subview had the selection focus after the mouse-up signal. It also shares the same input parameters as the [tableView:mouseDownInHeaderOfTableColumn:] message. Hence, the same code fragment shown in Listing 5 can be used to handle this message.

Another possible table event is a table row selection. Figure 14 shows the sequence diagram of that event. When the user clicks on a table row to select it, aTable first sends a [selectionShouldChangeInTableView:] message to aDelegate, which then responds with either a YES or NO value. If aDelegate responds with a YES, aTable then sends a [tableView:shouldSelectRow:] message to aDelegate, which again responds with either a YES or NO value. Finally, on the next mouse-up signal, aTable sends a [tableViewSelectionDidChanged:] message to aDelegate signalling the end of the sequence.

The above sequence, however, is correct if and only if the allowsMultipleSelection flag of aTable is set to NO, meaning that the user can only select one tale row at the time. Setting that flag to YES enables the user can select more than one table row. Then each time the user adds another row to the selection, aTable sends the [selectionShouldChangeInTableView:] message 12 times after the [tableViewIsChanging:] message.

Another special case is when the allowsEmptySelection flag of aTable is set to NO. This means that aTable automatically selects the first valid table row after receiving an awakeFromNib message. It will also send a single [tableViewSelectionDidChanged:] message informing aDelegate about the selection. Afterwards, other row selections follows the sequence described above.

Figure 15. Table row selection event.
Figure 15. Table row selection event.

Most of the delegate messages used by the table row selection are the same ones used in the table column selection. The only new message introduced by this event is the [tableView:shouldSelectRow:] message (Listing 10). This message allows the delegate to validate the row selection in progress. It provides two input parameters: aTable, the reference to the table view that generated the message; and aRow, the index of the selected table row as a signed integer. The delegate returns a YES to allow the row selection; otherwise, it returns a NO.

Listing 10. Verifying the table row selection.


- (BOOL)tableView:(NSTableView *)aTable shouldSelectRow:(int)aRow
{
     NSNumber      *id_table;
     BOOL          loc_chk;
      
      // parameter check
      if ((aCol != nil) && (aTable != nil))
      {
            // identify which table generated the message
            id_table = [aTable tag];
            
            // verify the delegate message
            // ...
      }
     // return the verification results
     return (loc_chk);
}

The fifth and final table event is a table cell edit; its sequence diagram is shown in Figure 15. This event occurs when the user double-clicks on a table cell thus initiating an inline editing session. aTable first sends a [selectionShouldChangeInTableView:] message to aDelegate. aDelegate confirms the change in selection by responding with a YES value. Then, aTable sends a [tableView:shouldSelectRow:] message, which aDelegate also responds with a YES value confirming the row selection. Finally, aTable sends a [tableViewSelectionIsChanging:] and a [tableViewSelectionDidChanged:] message informing aDelegate of the change in row selection. It then sends a [tableView:shouldEditTableColumn:row:] message, which ends the event sequence.

However, if the allowsEmptySelection flag is set to NO, aTable skips the first four delegate messages and sends only the [tableView:shouldEditTableColumn:row:] message to aDelegate when the user double-clicks on the automatically selected row. Also, if the user double-clicks on a different row, the same sequence of five messages shown in Figure 15 are sent to aDelegate.

Figure 16. Table cell edit event.
Figure 16. Table cell edit event.

The first four messages during the cell edit event are exactly the same ones generated during a table row selection event. However, only the cell edit event generates the [tableView:shouldEditTableColumn:row:] message (Listing 11). This message informs the delegate that a table data cell is about to be edited. It provides three input parameters that the delegate can use to determine the table cell being edited. If the delegate wants the inline-editing to proceed, it returns a YES value. Otherwise, it returns a NO, which aborts the edit session while keeping the selection focus on the double-clicked row.

Listing 11. Verifying the table edit message.


- (BOOL)tableView:(NSTableView *)aTable shouldEditTableColumn:(NSTableColumn *)aCol
              row:(int)aRow
{
     NSNumber      *id_table;
     NSString      *id_column;
     BOOL           loc_chk;
      
      // parameter check
      if ((aCol != nil) && (aTable != nil))
      {
            // identify which table and column generated the message
            id_table = [aTable tag];
            id_column = [aCol identifier];
            
            // verify the delegate message
            // ...
      }
     // return the verification results
     return (loc_chk);
}

Finally, the NSTableView class generates three other delegate messages, none of which fits in the five event categories featured here. The first message, [tableView:toolTipForCell:rec:tableColumn:row:mouseLocation:], is sent to the delegate when the cursor remains over a specific table data cell for a predetermined time (Listing 12). The delegate then returns an NSString object containing the message to be displayed by the table view as a tooltip.

The message provides six input parameters, only four of which are of any general significance. The first one, aTable, refers to the table view that generated the message. The aCol parameter refers to the column subview located underneath the cursor. The aRow parameter is the index of the table row underneath the cursor. Finally, the fourth parameter, aCell, refers to the table cell located underneath the cursor at the time of the message. Sending either an [intValue], [stringValue], or [objectValue] message to aCell returns the data contained by that cell.

Listing 12. Displaying a tooltip for a table cell


- (NSString *)tableView:(NSTableView *)aTable toolTipForCell:(NSCell *)aCell 
                   rect:(NSRectPointer)aRect tableColumn:(NSTableColumn *)aCol
                    row:(int)aRow mouseLocation:(NSPoint)aPos
{
     NSNumber *id_table;
     NSString *id_column, *loc_tip;
     id       *cell_data;
     
      // parameter check
      if ((aCol != nil) && (aTable != nil))
      {
            // identify which table and column generated the message
            id_table = [aTable tag];
            id_column = [aCol identifier];
            
            // retrieve the table cell data
            cell_data = [aCell objectValue];
            
            // prepare the tooltip message
            // ..
      }
     // return the tooltip message
     return (loc_tip);
}

The other two delegate messages, [tableView:heightOfRow:] and [tableView:willDisplayCell:forTableColumn:row:], are sent to the delegate during the data-source process (Figure 3). They provide the delegate some degree of control over the display of table data. The first message, [tableView:heightOfRow:] (Listing 13), is sent right before the data-source message, [tableView:objectValueForColumn:row:]. It enables the delegate to vary the row height so as to better fit the tabular data. The delegate then returns the new height value as a float.

The message provides two input parameters to the delegate. aTable refers to the table view that generated the message while aRow is the index of the table row about to be updated. Since the message itself is sent before any tabular data is available for display, the delegate has to directly query the data-source controller for the data corresponding to the specified row. In the example shown, the row data is provided by the data-source controller as an NSDictionary object through the [getDataForRow:] accessor. The delegate invokes this accessor and uses the returned data to determine the appropriate row height.

Listing 13. Calculating a new row height.


- (float)tableView:(NSTableView *)aTable heightOfRow:(int)aRow
{
     NSDictionary *row_dat;
     float         row_hgt;
     
     // retrieve the current row height
     row_hgt = [aTable rowHeight];
     
     // query the data-source for the row data
     row_dat = [aDatSrc getDataForRow:aRow];
     
     // determine the new row height
     //...
     
     // return the new row height
     return (aHgt);
}

The second delegate message, [tableView:willDisplayCell:forTableColumn:row] (Listing 14), is sent to the delegate right after [tableView:objectValueForColumn:row:]. It provides the same input parameters as [tableView:toolTipForCell:rect:tableColumn:row:mouseLocation:], minus those that are graphics-related. The message allows the delegate to customize the display attributes for each data cell. For instance, higher-priority data items would be displayed in bold-red fonts, while lower-priority ones in plain-blue fonts. However, it should not be used to directly alter the actual table data, especially if data-editing is supported. For such situations, use an NSFormatter object instead to customize the data format.

Listing 14. Verifying the tabular data to be displayed.


- (void)tableView:(NSTableView *)aTable willDisplayCell:(id)aCell
   forTableColumn:(NSTableColumn *)aCol 
              row:(int)aRow
{
     NSNumber *id_table;
     NSString *id_column, *loc_tip;
     id       *cell_data;
     
      // parameter check
      if ((aCol != nil) && (aTable != nil))
      {
            // identify which table and column generated the message
            id_table = [aTable tag];
            id_column = [aCol identifier];
            
            // retrieve the table cell data
            cell_data = [aCell objectValue];
            
            // configure the cell display attributes
            // ..
      }
}

Final Thoughts

The NSTableView class is a very versatile and feature-rich member of the Application Framework. We have only touched the bare basics on how to use this view to display tabular data in a Cocoa application. But we were able to demonstrate how to effectively implement a controller that would serve as a data source for the NSTableView class. We learned how to configure various visual aspects of the view using the appropriate modifier message.

We also learned how this view delegates various aspects of its display process to a separate controller. We were able to demonstrate how to implement either inline editing (through a data source) or panel editing using a separate controller. We demonstrated how to effectively bind a custom NSFormatter object to the NSTableView data cell, thus enabling the view to automatically format its tabular data to our own liking.

References

The following list of references are the official developer documentation used to write this article. The latest revisions of these documents can also be viewed online by visiting the Apple Developer website at http://developer.apple.com/documentation/Cocoa/Reference/ApplicationKit.

Jose Cruz has 10 years of experiences as a software engineer. He also writes articles for REALbasic Developer Magazine, MacTech, and Dr Dobbs Journal.


Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.