TweetFollow Us on Twitter

Table Techniques Taught Tastefully (part 2)

Volume Number: 18 (2002)
Issue Number: 10
Column Tag: Cocoa Development

Table Techniques Taught Tastefully (part 2)

Using NSTableView for Real-World Applications

by Dan Wood


Part one of this series of articles introduced the wonderful NSTableView class in Cocoa, going over the basics--displaying textual data, adding and deleting rows, and adjusting the columns of a table. With those techniques, you should be able to write code with some pretty useful displays. But there's so much more you can do with NSTableView, and this part of the series goes into some intermediate techniques that will help you feel like a "Table Jockey."

In this article, we'll cover a little bit more in the topic of deleting rows that we introduced in part 1. Then we'll introduce alphabetic type-ahead, which allows the user to start typing the first few letters of a row's relevant text to get the selection established. Next, we'll cover several aspects of sorting and reordering a table's rows, including sorting by clicking on a column header and drag-and-drop rearrangement. Finally, we'll cover exporting of data from a table via drag-and-drop and via the clipboard.

Be sure to follow along with the "TableTester" application (downloadable at, a program showing off most of the table features described in this series. It contains the source code corresponding to the techniques in this article as well as those in part one, in case you missed it the first time around.

Deleting Rows (The Sequel)

In part 1, we discussed how to delete the selected rows in a table, responding to a button or the Clear menu. How about deleting the selected rows when the delete key is pressed? This requires us to create a new subclass of NSTableView and override the keyDown: method to pass along the same deleteSelectedRowsInTableView: method to the data source. To create the user interface for this, we create a table but then set its custom class to our subclass, DeletableTableView, using the class inspector in Interface Builder.

Listing 1: DeletableTableView.m

Trap out delete keys and pass along the method to delete the selected rows.  Otherwise, just let the 
superclass handle the event.  The table must be "first responder" for this to be processed.

- (void)keyDown:(NSEvent *)theEvent
   NSString *keyString
      = [theEvent charactersIgnoringModifiers];
   unichar   keyChar = [keyString characterAtIndex:0];
   switch (keyChar)
      case 0177: // Delete Key
      case NSDeleteFunctionKey:
      case NSDeleteCharFunctionKey:
      if ( [self selectedRow] >= 0 && [[self dataSource]
            [[self dataSource]
         [super keyDown:theEvent];

A feature not implemented above is undoability; we'll leave this as an exercise for the reader.

Alphabetic Type-ahead for table selection

When lists are long, keyboard navigation -- the ability to start typing on the keyboard to navigate to the desired elements in a list -- is quite convenient. This functionality isn't built into NSTableView, but it is possible by creating a subclass that pays attention to the keystrokes when the table is active.

The algorithm is fairly straightforward. When the user presses a key, the key is appended to a string, and that string is used to try to select the closest row in the table. As more keys are pressed, the string builds up, and the selection becomes more refined. If a time interval of a couple of seconds passes with no keystrokes, the typing buffer is cleared, so the user can make a fresh selection.

To handle the keystrokes, we override keyDown: to merely send the interpretKeyEvents: message to self. According to the NSResponder documentation, this is how to intercept keystrokes; it will cause the insertText: method to be invoked, which we handle below.

Listing 2: TypeaheadTableView.m

Indicate that the keystrokes from the event should be interpreted.  The table needs to be "first 
responder" for this to be effective.

- (void)keyDown:(NSEvent*)inEvent
   [self interpretKeyEvents:
      [NSArray arrayWithObject:inEvent]];

We then implement insertText: to add the typed characters to our buffer of keystrokes, and go select the appropriate row based on what has been typed so far. Thinking in terms of Model-View-Controller partitioning, we send a message from the view (the NSTableView subclass) to the Controller (the table's delegate) to perform the selection, by invoking typeAheadString: inTableView: on the delegate. We will provide a sample implementation later.

We also queue up a message to send in the near future to clear out the keystroke buffer. But what is the magic number for the time delay before the buffer is cleared? You could hard-wire a constant, but that might not satisfy all users. Instead, you can base the time delay on the "Delay Until Repeat" setting in the System Preferences. The chapter from Inside Macintosh (Remember Inside Macintosh?) on the List Manager recommended two times that threshold value, but no more than two seconds. Whereas Carbon programs get the delay from the low memory global function LMGetKeyThresh(), this value is available via the defaults mechanism via the "InitialKeyRepeat" key.

Listing 3: TypeaheadTableView.m

Process the text by adding it to the typeahead buffer and selecting the appropriate row.

- (void)insertText:(id)inString
   // Make sure delegate will handle type-ahead message
   if ([[self delegate] respondsToSelector:
      // We clear it out after two times the key repeat rate "InitialKeyRepeat" user
      // default (converted from sixtieths of a second to seconds), but no more than two
      // seconds. This behavior is determined based on Inside Macintosh documentation
      // on the List Manager.
      NSUserDefaults *defaults
         = [NSUserDefaults standardUserDefaults];
      int keyThreshTicks
         = [defaults integerForKey:@"InitialKeyRepeat"];
      NSTimeInterval clearDelay
         = MIN(2.0/60.0*keyThreshTicks, 2.0);
      if ( nil == mStringToFind )
         mStringToFind = [[NSMutableString alloc] init];
         // lazily allocate the mutable string if needed.
      [mStringToFind appendString:inString];
      // Cancel any previously queued future invocations
      [NSObject cancelPreviousPerformRequestsWithTarget:self
      // queue an invocation of clearAccumulatingTypeahead for the near future.
      [self performSelector:
         withObject:nil afterDelay:clearDelay];
      // Let the table's delegate do something with the string.
      // We use stringWithString to make an autoreleased copy so for its use,
      // since we may clear out the original string below before it can be used.
      [[self delegate] typeAheadString:
         [NSString stringWithString:mStringToFind]

Clear out the string so our next typeahead will start from scratch.

- (void) clearAccumulatingTypeahead
   [mStringToFind setString:@""];   // clear out the queued string to find

All that remains now is the nitty-gritty of finding the appropriate row to select based on the string the user has typed so far. Usually, this will mean finding a row that is a close match, not necessarily an exact match, to the search string. In a list of U.S. States, for example, "M" would select Maine; "MI" would select Michigan; "MIS" and "MISS" would select Mississippi; "MISSO" would select Missouri.

In our TableTester code, we select based upon whichever is the currently sorted column. We search linearly (ideally, it should be a binary search if the data set is large) for the row containing the dictionary with the value of the current sorting key that is a best match, using a case insensitive comparison. We use the last row if no match was found, e.g. if the user typed "Z" in a table with no "Z" entries. That row is selected and the table view scrolled to make that selection visible.

Listing 4: SortingDelegate.m

Actually select the appropriate row based upon the string that has been typed.
(void) typeAheadString:(NSString *)inString
      inTableView:(NSTableView *)inTableView
   NSTableColumn *col = [inTableView highlightedTableColumn];
   if (nil != col)
      NSString *key = [col identifier];
      int i;
      for ( i = 0 ; i < [oData count] ; i++ )
         NSDictionary *rowDict = [oData objectAtIndex:i];
         NSString *compareTo = [rowDict objectForKey:key];
         NSComparisonResult order
            = [inString caseInsensitiveCompare:compareTo];
         if (order != NSOrderedDescending)
      // Make sure we're not overflowing the row count.
      if (i >= [oData count])
         i = [oData count] - 1;
      // Now select row i -- either the one we found, or the last row if not found.
      [inTableView selectRow:i byExtendingSelection:NO];
      [inTableView scrollRowToVisible:i];

One final note on typeahead: CodeWarrior has a nice feature of showing you the typeahead buffer so you can see what you have typed. In the TableTester program, but not listed here, are some minor additions to the TypeaheadTableView class that will display the current typeahead buffer if you have hooked up a text field to the appropriate outlet.

Displaying More Than Just Strings

Usually, a table cell displays a piece of plain text, just an NSString returned in tableView: objectValueForTableColumn: row: But this method is designed to return "id", meaning any object. Without any extra effort, you can return an NSAttributedString or NSNumber instead of an NSString. And with just a little bit of setup, you can display other kinds of cells such as images and buttons.

First, you need to set the table's columns to use a different cell type. In a convenient initialization method, such as your controller's awakeFromNib method, just allocate a new cell object, such as an NSButtonCell or an NSImageCell. You may need to adjust attributes of your cell (for instance, setting a button cell's type and image position). Then, find the NSTableColumn object corresponding to the column to affect, and replace the default text cell with your newly allocated instance, using setDataCell:. (Alternately, you can create a disembodied control in your nib file that represents the prototype cell and hook it all up in IB!)

In our TableTester program, we allocate a button cell and an image cell. The button cell is set up to be a checkbox (NSSwitchButton); the image cell doesn't need any adjustment. Finally, the button and image cells are hooked up to the columns.

Listing 5: CellDelegate.m

Set the table columns to use button and image cells rather than the default text cells.
- (void)awakeFromNib
   // Allocate the cells
   NSButtonCell *buttonCell
      = [[[NSButtonCell alloc] init] autorelease];
   NSImageCell  *imageCell
      = [[[NSImageCell alloc] init] autorelease];
   // Find the columns
   NSTableColumn *buttonColumn
      = [oTable tableColumnWithIdentifier:@"checked"];
   NSTableColumn *imageColumn
      = [oTable tableColumnWithIdentifier:@"icon"];
   // Set up the button cell and install into the column
   [buttonCell setButtonType:NSSwitchButton];
   [buttonCell setImagePosition:NSImageOnly];
   [buttonCell setTitle:@""];
   [buttonColumn setDataCell:buttonCell];
   // Install the image cell
   [imageColumn setDataCell:imageCell];

To display these non-textual cells, you need to provide appropriate data in tableView: objectValueForTableColumn: row: and perhaps also make per-cell adjustments in tableView: willDisplayCell: forTableColumn: row:. In TableTester, we implement the data source and provide an appropriate object based on the given column. For the "icon" column, we provide an NSImage object based on the name in the data table. (This relies on their being an image available for the name; we have a few sample images created with Stick Software's "Aquatint" program bundled with TableTester.) For the "checked" column, we provide an NSNumber object corresponding to the state of the checkbox. For the "name" column, we build up an attributed string to display in the default string cell. (And in the source code for TableTester, not shown here, we also have a cell for a "relevance" control, which uses a custom cell class.)

Listing 6: CellSource.m

tableView: objectValueForTableColumn: row:
Provide the data for a given cell. We provide different data depending on which column is passed in 
as a parameter.

(id)tableView:(NSTableView *)inTableView
      objectValueForTableColumn:(NSTableColumn *)inTableColumn
   id result = nil;
   // Start out with the string that the SimpleSource returns from the dictionary
   id stringValue = [super tableView:inTableView
      objectValueForTableColumn:inTableColumn row:inRowIndex];
   // Now, handle special cases depending on the table column identifier.
   NSString *identifier = [inTableColumn identifier];
   if ([identifier isEqualToString:@"icon"])
      if (nil != stringValue)
         result = [NSImage imageNamed:stringValue];
   else if ([identifier isEqualToString:@"checked"])
      // Return NSNumber of 1 or 0.  The line below builds the NSNumber
      // from from the string or NSNumber currently in the dictionary.
      result = [NSNumber numberWithInt:[stringValue intValue]];
   else if ([identifier isEqualToString:@"name"])
      // Really, these dictionaries should be created once and cached....
      NSDictionary *plainAttr
         = [NSDictionary dictionaryWithObject:
           [NSFont systemFontOfSize:[NSFont systemFontSize]]
      NSDictionary *boldAttr
         = [NSDictionary dictionaryWithObject:
        [NSFont boldSystemFontOfSize:[NSFont systemFontSize]]
      // Return an attributed string, with the first character boldface.
      result = [[[NSMutableAttributedString alloc] init]
      [result appendAttributedString:
         [[[NSAttributedString alloc]
            initWithString:[stringValue substringToIndex:1]
                  attributes:boldAttr] autorelease]];
      [result appendAttributedString:
         [[[NSAttributedString alloc]
            initWithString:[stringValue substringFromIndex:1]
                  attributes:plainAttr] autorelease]];
      // If not the special cases, we've gotten the string result from the superclass.
      result = stringValue;
   return result;

Sorting Tables

Displaying a table with its data sorted can enhance readability greatly. What's even more useful is if you give the user the ability to decide which column to sort a table by, and which direction to sort in. Astute readers will remember an article by Andrew Stone that covered sorting of tables in the August 2002 issue of MacTech; this section takes a slightly different approach (and offers Mac OS X 10.2 compatibility too).

To support column sorting, you need to implement tableView: didClickTableColumn: in your delegate. Your code would sort the data and redisplay it, highlighting the clicked column to provide feedback to the user as to what column the data is sorted by. If the user clicks on the sorted column a second time, the direction of the sort should change, and a small graphic in the column should indicate whether the table is sorted ascending or descending. For this to happen, you need to implement a bit of code.

First, let's deal with the actual sorting. NSArray and NSMutableArray provide a number of methods for sorting. The most convenient methods you can use are the methods sortUsingFunction: (if you have an NSMutableArray) or sortedArrayUsingFunction: (if you have an immutable NSArray). With these methods, you provide a C function that knows how to compare two objects and is given an arbitrary context for performing the sort. In our sample case, we use a simple structure specifying the key to sort upon, and the direction to sort in. Our function, ORDER_BY_CONTEXT, sets up the order for the comparison based upon the sorting direction, and then invokes either caseInsensitiveCompare: or compare: between the two objects. The former is useful for strings; the latter is useful for numbers or dates. Our method sortData invokes sortUsingFunction: on the data array using the current sorting key and direction, then causes the table to redisplay itself with the reloadData method.

Listing 7: SortingDelegate.m

C function to return the sort ordering for the given two objects and the given context.
typedef struct { NSString *key; BOOL descending; }
int ORDER_BY_CONTEXT (id left, id right, void *ctxt)
   SortContext *context = (SortContext*)ctxt;
   int order = 0;
   id key = context->key;
   if (0 != key)
      id first,second;   // the actual objects to compare
      if (context->descending)
         first  = [right objectForKey:key];
         second = [left  objectForKey:key];
         first  = [left  objectForKey:key];
         second = [right objectForKey:key];
      if ([first respondsToSelector:
         order = [first caseInsensitiveCompare:second];
      else   // sort numbers or dates
         order = [(NSNumber *)first compare:second];
   return order;

Sort the data array based on the current sorting key (column) and sorting direction.

- (void) sortData
   SortContext ctxt={ mSortingKey, mSortDescending };
   [oData sortUsingFunction:ORDER_BY_CONTEXT context:&ctxt];
   [oTable reloadData];

Since we want to indicate the direction of sorting, we display a little triangle in the table column using the -[NSTableView setIndicatorImage: inTableColumn:] method. In Mac OS X 10.2, Apple provides official access to these images. But if you want your sorting-table application to work under 10.1, you have a couple of options. One is to include the images in your application's executable; another is to carefully make use of a private (undocumented) method in NSTableView. Using private methods is something you have to be very careful about, because Apple doesn't support them, and they could go away at any time. To make sure that the sample code will work on both 10.1 and 10.2, we test for compatibility using respondsToSelector: to fail gracefully, then add class methods to NSTableView called ascendingSortIndicator and descendingSortIndicator to safely return the images we'll need.

Listing 8: NSTableView+util.m

Safely return the ascending sort triangle image; works on both 10.1 and 10.2.
+ (NSImage *) ascendingSortIndicator
   NSImage *result
      = [NSImage imageNamed:@"NSAscendingSortIndicator"];
   if (nil == result
      && [[NSTableView class] respondsToSelector:
      result = [NSTableView _defaultTableHeaderSortImage];
   return result;

Safely return the descending sort triangle image; works on both 10.1 and 10.2.

+ (NSImage *) descendingSortIndicator
   NSImage *result
      = [NSImage imageNamed:@"NSDescendingSortIndicator"];
   if (nil == result
      && [[NSTableView class] respondsToSelector:
         = [NSTableView _defaultTableHeaderReverseSortImage];
   return result;

Now to specify how clicking on a column sorts by that column. If it's a click on an already selected column, we reverse the sorting direction. Our method sortByColumn: is fairly straightforward; given a table column, we switch sort order if it's a click on the previously saved column; otherwise we change to a new sorting column. We invoke the column sorting method in the delegate method tableView: didClickTableColumn: to respond to column clicks. In a full application, you may want to store a preference of the sorted column and initially sort appropriately, perhaps calling sortByColumn: in your awakeFromNib method.

Listing 9: SortingDelegate.m

Sort the table by the given column, changing sort direction if already sorted by that column.
- (void)sortByColumn:(NSTableColumn *)inTableColumn
   if (mSortingColumn == inTableColumn)
      // User clicked same column, change sort order
      mSortDescending = !mSortDescending;
      // User clicked new column, change old/new column headers,
      // save new sorting column, and re-sort the array.
      mSortDescending = NO;
      if (nil != mSortingColumn)
         [oTable setIndicatorImage:nil
            inTableColumn: mSortingColumn];
      [self setSortingKey:[inTableColumn identifier]];
      [self setSortingColumn:inTableColumn];
      [oTable setHighlightedTableColumn:inTableColumn];
   [oTable setIndicatorImage: (mSortDescending
            ? [NSTableView descendingSortIndicator]
            : [NSTableView ascendingSortIndicator])
         inTableColumn: inTableColumn];
   // Actually sort the data
   [self sortData];

tableView: didClickTableColumn:
User clicked on a table column, so sort (or invert the sort) by that column.

      didClickTableColumn:(NSTableColumn *)inTableColumn
   [self sortByColumn:inTableColumn];

Maintaining Selection for a Sort

In the above example, one potential problem is what will happen if any rows are selected when you sort the table. The selected rows will remain the same positionally, but the items selected will no longer be the same. You could clear out any selection when you sort, but it's more useful to maintain the selected items though a sort. The method saveSelectionFromTable: gathers up the array items into an NSSet, and restoreSelection: toTable: finds those items after the array has been sorted and reselects them. These methods should handle simple cases, though the selection restoration code may not perform well for large arrays since -[NSArray indexOfObjectIdenticalTo:] is essentially a linear search.

Listing 10: SortingDelegate.m

Create a set that represents the current selection from a table, for later restoral.
- (NSSet *) saveSelectionFromTable:(NSTableView *)inTableView
   NSMutableSet *result = [NSMutableSet set];
   NSEnumerator *theEnum
      = [inTableView selectedRowEnumerator];
   NSNumber *rowNum;
   while (nil != (rowNum = [theEnum nextObject]) )
      id item = [oData objectAtIndex:[rowNum intValue]];
      [result addObject:item];
   return result;

restoreSelection: toTable:
Restore the selection from the given set of row objects after a table has been sorted.

- (void) restoreSelection:(NSSet *)inSelectedItemNums toTable:(NSTableView *)inTableView
   NSEnumerator *theEnum
      = [inSelectedItemNums objectEnumerator];
   id item;
   int savedLastRow;
   [inTableView deselectAll:nil];
   while (nil != (item = [theEnum nextObject]) )
      int row = [oData indexOfObjectIdenticalTo:item];
      // look for an exact match, which is OK here.
      if (NSNotFound != row)
         [inTableView selectRow:row byExtendingSelection:YES];
         savedLastRow = row;
   [inTableView scrollRowToVisible:savedLastRow];

To make use of these selection methods, we just save the selection before sorting and restore it afterwards. This is the new implementation of sortData from above.

Listing 11: SortingDelegate.m

Sort the data, but this time, save the selection before the sort and restore it afterwards.
- (void) sortData
   SortContext ctxt={ mSortingKey, mSortDescending };
   NSSet *oldSelection = [self saveSelectionFromTable:oTable];
   // sort the NSMutableArray
   [oData sortUsingFunction: ORDER_BY_CONTEXT context:&ctxt];
   [oTable reloadData];
   [self restoreSelection:oldSelection toTable:oTable];

Drag and Drop to Rearrange Rows

Some tables benefit from the ability to drag rows around to reorder the table's contents. For instance, the "International" panel of the System Preferences allows you to specify the languages you prefer to use when using the Mac.

According to the documentation on the NSTableDataSource informal protocol, there are three methods you would implement in your table's data source to facilitate drag and drop. One is for grabbing the data when you drag; one validates whether data can be dropped on your table; one performs the drop. It sounds simple, but there are always subtleties to work through.

If your table is going to handle drag and drop, you need to decide what operations that entail. In this segment, we are merely rearranging rows in the table, then it is simplest to merely keep track of the row index (or indexes) being dragged, so that your code can directly manipulate the data array's ordering.

The first of the three methods, tableView: writeRows: toPasteboard: is tasked with putting data corresponding to the given rows that are being dragged into a pasteboard. In Cocoa, there are multiple pasteboards available, and each pasteboard can contain multiple kinds of data. In our case, we put a representation of which row indexes were dragged, for the purposes of mere row reordering. This is identified as a constant string, "MyRowListPasteboardType", identified in the code as kPrivateRowPBType. (If your program were to have multiple table views -- where it would be possible to drag from one table to the other -- you'd have to take care to specify separate pasteboard data types, and/or encode the source of the row indexes, so you wouldn't inadvertently mix apples and oranges.)

To encode is the list of dragged row indexes, we just use the NSArchiver class to turn the NSArray we are handed containing all of the row indexes into a single NSData object.

Listing 12: DraggableSource.m

tableView: writeRows: toPasteboard:
Put a list of the given rows onto a private pasteboard.
- (BOOL)tableView:(NSTableView *)inTableView writeRows:(NSArray*)inRows 
   NSData *archivedRowData
      = [NSArchiver archivedDataWithRootObject:inRows];
   [inPasteboard declareTypes:
         [NSArray arrayWithObjects:kPrivateRowPBType, nil]
   [inPasteboard setData: archivedRowData
   return YES;

The next method, tableView: validateDrop: proposedRow: proposedDropOperation:, is needed to validate whether a drop can occur; this is called repeatedly as the user drags over the table view. Tables can accept drops onto a row, or between rows; in our case, we only want to accept drops between rows (the given NSTableViewDropOperation must be NSTableViewDropAbove), and we only want to accept the drop if the clipboard has our own private data type.

Listing 13: DraggableSource.m

tableView: validateDrop: proposedRow: proposedDropOperation:
Determine whether a drop can take place, and how it will be treated.
      validateDrop:(id <NSDraggingInfo>)inInfo
   // Look for our private type for reordering rows.
   NSString *type
      = [[inInfo draggingPasteboard]
               arrayWithObjects:kPrivateRowPBType, nil]];
   if (inOperation == NSTableViewDropAbove
      && [type isEqualToString:kPrivateRowPBType])
      return NSDragOperationMove;
   return NSDragOperationNone;

The final method, tableView: acceptDrop: row: dropOperation: actually performs the drop. This method checks the pasteboard type available, and if it is our private identifier representing row indexes, it performs the data rearrangement. It works by copying out the appropriate rows of data from our data array, replacing them with special NSNull values; then it inserts these data rows into the table; finally it compacts the table by removing the NSNull placeholders.

Listing 14: DraggableSource.m

tableView: acceptDrop: row: dropOperation:
Handle a drop. If it's our private pasteboard listing the rows to move, actually move the data.

- (BOOL)tableView:(NSTableView*)inTableView
      acceptDrop:(id <NSDraggingInfo>)inInfo
   // Look for our private type for reordering rows.
   NSString *type = [[inInfo draggingPasteboard]
         arrayWithObjects:kPrivateRowPBType, nil]];
   if ([type isEqualToString:kPrivateRowPBType])
      NSData *archivedRowData
         = [[inInfo draggingPasteboard]
      NSArray *rows = [NSUnarchiver
      NSMutableArray *movedRows
         = [NSMutableArray arrayWithCapacity:[rows count]];
      NSEnumerator *theEnum = [rows objectEnumerator];
      id theRowNumber;
      // First collect up all the selected rows, then put null where it was in the array
      while (nil != (theRowNumber = [theEnum nextObject]) )
         int row = [theRowNumber intValue];
         [movedRows addObject:[oData objectAtIndex:row]];
         [oData replaceObjectAtIndex:row
            withObject:[NSNull null]];
      // Then insert these data rows into the array
      [oData replaceObjectsInRange:NSMakeRange(inRow, 0)
      // Now, remove the NSNull placeholders
      [oData removeObjectIdenticalTo:[NSNull null]];
      // And refresh the table.  (Ideally, we should turn off any column highlighting)
      [inTableView deselectAll:nil];
      [inTableView reloadData];
   return YES;

One last step remains. The table view must register itself in being interested in the pasteboard type representing our row indexes; a good place to perform this is in your awakeFromNib method.

   [oTable registerForDraggedTypes:
      [NSArray arrayWithObjects:kPrivateRowPBType,nil]];

Drag and Drop of Data

It can also be useful to drag data from the tables into other applications, or import data via drag and drop. In this segment, we'll explore data export via drag and drop, leaving importing of data as another proverbial "exercise for the reader."

For dragging data out, only the first method, tableView: writeRows: toPasteboard:, is affected, as it packages up the data to export to another program. (If you were to allow dropping onto your table views from external sources, such as cells dropped in from a spreadsheet, you would want to modify tableView: validateDrop: proposedRow: proposedDropOperation: and tableView: acceptDrop: row: dropOperation: to accept other types of pasteboard data, and register for those pasteboard types in your awakeFromNib method.)

Our TableTester program adds two additional pasteboard types to the pasteboard in addition to the private type for rearranging rows: NSStringPboardType (generic text) and NSTabularTextPboardType (tabular text, as for a spreadsheet).

To build up the strings, we enumerate through each of the rows; for each row, we enumerate through all the columns. Each row of data fills up the buffer with strings for each cell, separated by a tab or a newline, with a final extra newline after each row. If your application needed the data formatted differently (for instance, always separated by tabs, or always in a specific column ordering regardless of the current display, or as rich text), you would modify this code to build the appropriately structured data.

Listing 15: DraggableSource.m

tableView: writeRows: toPasteboard:
Write the text data on the given rows onto the pasteboard.
(BOOL)tableView:(NSTableView *)inTableView
   NSData *archivedRowData
      = [NSArchiver archivedDataWithRootObject:inRows];
   NSArray *tableColumns = [inTableView tableColumns];
   NSMutableString *tabsBuf = [NSMutableString string];
   NSMutableString *textBuf = [NSMutableString string];
   NSEnumerator *rowEnum = [inRows objectEnumerator];
   NSNumber *rowNumber;
   while (nil != (rowNumber = [rowEnum nextObject]) )
      int row = [rowNumber intValue];
      NSEnumerator *colEnum = [tableColumns objectEnumerator];
      NSTableColumn *col;
      while (nil != (col = [colEnum nextObject]) )
         id columnValue
            = [self tableView:inTableView
               objectValueForTableColumn:col row:row];
         NSString *columnString = @"";
         if (nil != columnValue)
            columnString = [columnValue description];
         [tabsBuf appendFormat:@"%@\t",columnString];
         if (![columnString isEqualToString:@""])
            [textBuf appendFormat:@"%@\n",columnString];
      // delete the last tab.  (But don't delete the last CR)
      if ([tabsBuf length])
         [tabsBuf deleteCharactersInRange:
            NSMakeRange([tabsBuf length]-1, 1)];
      // Append newlines to both tabular and newline data
      [tabsBuf appendString:@"\n"];
      [textBuf appendString:@"\n"];
   // Delete the final newlines from the text and tabs buf.
   if ([tabsBuf length])
      [tabsBuf deleteCharactersInRange:
         NSMakeRange([tabsBuf length]-1, 1)];
   if ([textBuf length])
      [textBuf deleteCharactersInRange:
         NSMakeRange([textBuf length]-1, 1)];
   // Set up the pasteboard
   [inPasteboard declareTypes:
      [NSArray arrayWithObjects:NSTabularTextPboardType,
         NSStringPboardType, kPrivateRowPBType, nil] owner:nil];
   // Put the data into the pasteboard for our various types
   [inPasteboard setString:[NSString stringWithString:textBuf]
   [inPasteboard setString:[NSString stringWithString:tabsBuf]
   [inPasteboard setData: archivedRowData forType:
   return YES;

One obscure trick remains. In order for you to be able to drag data outside of your application, you need to override a method in NSTableView. Your table view must therefore be of a custom class. If you don't override this method, you will not be able to drag data out of a table into another application! Hopefully Apple will fix this in a future version of Mac OS X.

Listing 16: TypeaheadTableView.m

Allow drags outside of an application.
- (NSDragOperation)draggingSourceOperationMaskForLocal:
   if (isLocal) return NSDragOperationEvery;
   else return NSDragOperationCopy;

Copying Rows to the Clipboard

With drag and drop supported, it's actually quite easy to add the capability to copy selected rows of a table to the clipboard. We employ the method tableView: writeRows: toPasteboard:, passing in the general pasteboard, to respond to the Copy menu item. We check to make sure that the table's data source implements that method, so this method will fail gracefully if the current table doesn't support the clipboard.

Listing 17: AppController.m

Handle the request to copy rows from the table.
- (IBAction) copy:(id)sender
   // Get the NSTableView we want to copy from.  In this case, we determine the
   // "current" table view by getting the tab view item's initialFirstResponder,
   // which is set in the nib.
   id currentTable
      = [[oTabView selectedTabViewItem] initialFirstResponder];
   // Now put the selected rows in the general pasteboard.
   if ([[currentTable dataSource] respondsToSelector:
      (void) [[currentTable dataSource] tableView:currentTable
         writeRows:[currentTable selectedRows]
         toPasteboard:[NSPasteboard generalPasteboard]];

It's actually that simple! All that is missing is a method in NSTableView called selectedRows, to return an array of row indexes. This is fixed quickly by adding a category method to NSTableView.

Listing 18: NSTableView+util.m

Return an array of the selected row numbers of the table.
- (NSArray *) selectedRows
   NSEnumerator *theEnum = [self selectedRowEnumerator];
   NSNumber *rowNumber;
   NSMutableArray *rowNumberArray = [NSMutableArray
      arrayWithCapacity:[self numberOfSelectedRows]];
   while (nil != (rowNumber = [theEnum nextObject]) )
      [rowNumberArray addObject:rowNumber];
   return rowNumberArray;

Until We Meet Again

If you've made to the end of part two, congratulations -- you can go forth and create some amazingly rich table interfaces. But there are still more cool things you can do with NSTableView, and this is why there's another part in the series on the way. Tune in next month for part three, in which we'll some advanced techniques, including the technique for correctly displaying those trendy striped tables that you see in the "iApps," and a subclass that merges certain cells together into wider cells.

Dan Wood once took an introductory Arabic class, but nobody in the room knew what language they were being taught. He likes to buy fruits and vegetables from the farmer's market on Tuesday mornings. He missed the last two days of WWDC this year due to the birth of his son. He is the author of Watson, an application written in Cocoa. Dan thanks Chuck Pisula at Apple for his technical help with this series, and acknowledges online code fragments from John C. Randolph, Stephane Sudre, Ondra Cada, Vince DeMarco, Harry Emmanuel, and others. You can reach him at


Community Search:
MacTech Search:

Software Updates via MacUpdate

VirtualBox 5.1.20 - x86 virtualization s...
VirtualBox is a family of powerful x86 virtualization products for enterprise as well as home use. Not only is VirtualBox an extremely feature rich, high performance product for enterprise customers... Read more
Arq 5.7.9 - Online backup to Google Driv...
Arq is super-easy online backup for Mac and Windows computers. Back up to your own cloud account (Amazon Cloud Drive, Google Drive, Dropbox, OneDrive, Google Cloud Storage, any S3-compatible server... Read more
Vienna 3.1.10 :d05d7a5d: - RSS and Atom...
Vienna is a freeware and Open-Source RSS/Atom newsreader with article storage and management via a SQLite database, written in Objective-C and Cocoa, for the OS X operating system. It provides... Read more
WhiteCap 6.7 - Visual plug-in for iTunes...
WhiteCap is a sleek and sophisticated music visualizer and screensaver that features futuristic, wireframe mesh visuals with dynamic backgrounds and colors. WhiteCap contains thousands of visual... Read more
Dropbox 24.4.16 - Cloud backup and synch...
Dropbox is an application that creates a special Finder folder that automatically syncs online and between your computers. It allows you to both backup files and keep them up-to-date between systems... Read more
Amazon Chime 4.2.5645 - Amazon-based com...
Amazon Chime is a communications service that transforms online meetings with a secure, easy-to-use application that you can trust. Amazon Chime works seamlessly across your devices so that you can... Read more
Notion 0.1.8 - A unified workspace for m...
Notion is the unified workspace for modern teams. Features: Integration with Slack Documents Wikis Tasks Note: This application contains in-app and/or external module purchases. Version 0.1.8:... Read more
Google Chrome 58.0.3029.81 - Modern and...
Google Chrome is a Web browser by Google, created to be a modern platform for Web pages and applications. It utilizes very fast loading of Web pages and has a V8 engine, which is a custom built... Read more
Notion 0.1.8 - A unified workspace for m...
Notion is the unified workspace for modern teams. Features: Integration with Slack Documents Wikis Tasks Note: This application contains in-app and/or external module purchases. Version 0.1.8:... Read more
WhiteCap 6.7 - Visual plug-in for iTunes...
WhiteCap is a sleek and sophisticated music visualizer and screensaver that features futuristic, wireframe mesh visuals with dynamic backgrounds and colors. WhiteCap contains thousands of visual... Read more

Latest Forum Discussions

See All

ChordFlow (Music)
ChordFlow 1.0.0 Device: iOS Universal Category: Music Price: $6.99, Version: 1.0.0 (iTunes) Description: ChordFlow is a chord sequencer with a unique 4-track polyphonic arpeggiator, extensive chord library, MIDI out and Ableton Link... | Read more »
The Walking Dead: A New Frontier is out...
The newest season of Telltale Games'The Walking Dead is well underway. After the release of the third episode, "Above the Law" about a month ago, episode four, "Thicker Than Water" is hot and ready for more zombies and gut-wrenching emotional... | Read more »
Best games we played this week
Another week, another new wave of mobile games do dive into. We've dug through the list of apps that came out this week to tell you which apps are worth your sweet time. And while there weren't too many games this week, there were some big ones.... | Read more »
Vignettes (Games)
Vignettes 1.0.1 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0.1 (iTunes) Description: Vignettes is a casual but unique exploration game without text or characters, where objects shapeshift as you spin them around... | Read more »
Get Me Outta Here is an 80s retro shoote...
Are you ready to fight some aliens? Because Crescent Moon Games has released the retro shooter Get Me Outta Here on iOS devices today. [Read more] | Read more »
Get a bunch of Apple productivity apps f...
If you're an Apple Mac owner, you're probably aware of the host of Apple productivity apps the company includes in all new Mac purchases. Apps like iMovie, Keynote, and of course, GarageBand. While you used to be able to also buy these apps... | Read more »
Terra Mystica (Games)
Terra Mystica 1.03 Device: iOS Universal Category: Games Price: $9.99, Version: 1.03 (iTunes) Description: Short Summary:≈≈≈≈≈≈≈≈≈≈≈≈≈ | Read more »
Ms. Spell (Games)
Ms. Spell 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: Cast spells and battle monsters in this turn based game, that has you delving into ever the changing Dreadwood to retrieve the lost... | Read more »
Invert - A Minimal Puzzle Game (Games)
Invert - A Minimal Puzzle Game 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: Invert is a minimalist puzzle game for fans of brain teasers, logic puzzles, and Rubik's Cube fiddlers. | Read more »
Evergrow: Paper Forest (Games)
Evergrow: Paper Forest 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: Solve puzzles in the handcrafted forests of Evergrow through the eyes of an imaginative child and his parents. Discover... | Read more »

Price Scanner via

15-inch Touch Bar MacBook Pros, Apple refurbi...
Apple is offering Certified Refurbished 2016 15″ Touch Bar MacBook Pros for $360 to $420 off original MSRP. An Apple one-year warranty is included with each model, and shipping is free: - 15″ 2.6GHz... Read more
13-inch MacBook Airs on sale for up to $150 o... has 13″ MacBook Airs on sale for up to $150 off MSRP including free shipping: - 13″ 1.6GHz/128GB MacBook Air (sku MMGF2LL/A): $869.99 $130 off MSRP - 13″ 1.6GHz/256GB MacBook Air (sku... Read more
15-inch Touch Bar MacBook Pros on sale for $1...
B&H Photo has the new 2016 15″ Apple Touch Bar MacBook Pros in stock today and on sale for up to $200 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 15″ 2.7GHz Touch Bar... Read more
15-inch 2.7GHz Touch Bar MacBook Pros on sale...
Amazon has 2016 15″ 2.7GHz Apple Touch Bar MacBook Pros in stock today and on sale for $150-$200 off MSRP. Shipping is free: - 15″ 2.7GHz Touch Bar MacBook Pro Space Gray (sku MLH42LL/A): $2599 $200... Read more
Apple now offering Certified Refurbished 13-i...
Apple is now offering Certified Refurbished 2016 13″ Touch Bar MacBook Pros for $270-$300 off original MSRP. An Apple one-year warranty is included with each model, and shipping is free: - 13″ 2.9GHz... Read more
MyGiHealth Digestive Symptom Tracker Version...
My Total Health, Inc. has announced the release of MyGiHealth 2.1, an important update to their digestive symptom tracker developed exclusively for iPhone, iPad and iPod touch devices. MyGiHealth is... Read more
Galaxy S8 Materials Costs Highest by Far Comp...
The new Samsung Galaxy S8 equipped with 64 gigabytes (GB) of NAND flash memory carries a bill of materials (BOM) cost that comes out to US$301.60, much higher than for previous versions of the... Read more
iCarMode 4.0 Car Dashboard App For iOS Integr...
Indie developer Diego Resnik has announced the release of iCarMode 4.0, an update to his productivity app developed for iOS devices. iCarMode has positioned itself as a true car dashboard app,... Read more
How to save $150+ on Apple’s 13-inch 2.0GHz n...
Apple Authorized Reseller B&H Photo has non-Touch Bar 13″ 2.0GHz MacBook Pros on sale for $150 off MSRP for a limited time. Shipping is free, and B&H charges NY sales tax only: - 13″ 2.0GHz... Read more
15-inch 2.2GHz Retina MacBook Pro, Apple refu...
Apple has Certified Refurbished 2015 15″ 2.2GHz Retina MacBook Pros available for $1699. That’s $300 off MSRP, and it’s the lowest price available for a 15″ MacBook Pro. An Apple one-year warranty is... Read more

Jobs Board

*Apple* Media Products - Commerce Engineerin...
Apple Media Products - Commerce Engineering Manager Job Number: 57037480 Santa Clara Valley, California, United States Posted: Apr. 18, 2017 Weekly Hours: 40.00 Job Read more
*Apple* Mac Computer Technician - GeekHampto...
…complex computer issues over the phone and in person? GeekHampton, Long Island's Apple Premium Service Provider, is looking for you! Come work with our crew Read more
*Apple* Mobile Master - Best Buy (United Sta...
**493714BR** **Job Title:** Apple Mobile Master **Location Number:** 001024-Weatherford-Store **Job Description:** **What does a Best Buy Apple Mobile Master Read more
Best Buy *Apple* Computing Master - Best Bu...
**496963BR** **Job Title:** Best Buy Apple Computing Master **Location Number:** 001061-Marina-Store **Job Description:** **What does a Best Buy Apple Computing Read more
*Apple* Mobile Master - Best Buy (United Sta...
What does a Best Buy Apple Mobile Master do? At Best Buy, our mission is to leverage the unique talents and passions of our employees to inspire, delight, and enrich Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.