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

Introduction

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 www.karelia.com/tabletester/), 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

keyDown:
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]
         respondsToSelector:
            @selector(deleteSelectedRowsInTableView:)])
         {
            [[self dataSource]
               deleteSelectedRowsInTableView:self];
         }
         break;
      default:
         [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

keyDown:
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

insertText:
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:
      @selector(typeAheadString:inTableView:)])
   {
      // 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
         selector:@selector(clearAccumulatingTypeahead)
         object:nil];
   
      // queue an invocation of clearAccumulatingTypeahead for the near future.
      [self performSelector:
         @selector(clearAccumulatingTypeahead)
         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]
         inTableView:self];
   }
}

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

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

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

typeAheadString:inTableView:
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)
         {
            break;
         }
      }
      // 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

awakeFromNib
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
      row:(int)inRowIndex
{
   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]]
            forKey:NSFontAttributeName];
      NSDictionary *boldAttr
         = [NSDictionary dictionaryWithObject:
        [NSFont boldSystemFontOfSize:[NSFont systemFontSize]]
            forKey:NSFontAttributeName];
      // Return an attributed string, with the first character boldface.
      result = [[[NSMutableAttributedString alloc] init]
                     autorelease];
      [result appendAttributedString:
         [[[NSAttributedString alloc]
            initWithString:[stringValue substringToIndex:1]
                  attributes:boldAttr] autorelease]];
      [result appendAttributedString:
         [[[NSAttributedString alloc]
            initWithString:[stringValue substringFromIndex:1]
                  attributes:plainAttr] autorelease]];
   }
   else
   {
      // 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

ORDER_BY_CONTEXT
C function to return the sort ordering for the given two objects and the given context.
typedef struct { NSString *key; BOOL descending; }
   SortContext;
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];
      }
      else
      {
         first  = [left  objectForKey:key];
         second = [right objectForKey:key];
      }
      if ([first respondsToSelector:
            @selector(caseInsensitiveCompare:)])
      {
         order = [first caseInsensitiveCompare:second];
      }
      else   // sort numbers or dates
      {
         order = [(NSNumber *)first compare:second];
      }
   }
   return order;
}

sortData
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

ascendingSortIndicator
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:
         @selector(_defaultTableHeaderSortImage)])
   {
      result = [NSTableView _defaultTableHeaderSortImage];
   }
   return result;
}

descendingSortIndicator
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:
         @selector(_defaultTableHeaderReverseSortImage)])
   {
      result
         = [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

sortByColumn:
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;
   }
   else
   {
      // 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.

 (void)tableView:(NSTableView*)inTableView
      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

saveSelectionFromTable
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

sortData
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 
toPasteboard:(NSPasteboard*)inPasteboard
{
   NSData *archivedRowData
      = [NSArchiver archivedDataWithRootObject:inRows];
   [inPasteboard declareTypes:
         [NSArray arrayWithObjects:kPrivateRowPBType, nil]
      owner:nil];
   [inPasteboard setData: archivedRowData
      forType:kPrivateRowPBType];
   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.
(NSDragOperation)tableView:(NSTableView*)inTableView
      validateDrop:(id <NSDraggingInfo>)inInfo
      proposedRow:(int)inRow
      proposedDropOperation:
         (NSTableViewDropOperation)inOperation
{
   // Look for our private type for reordering rows.
   NSString *type
      = [[inInfo draggingPasteboard]
            availableTypeFromArray:[NSArray
               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
      row:(int)inRow
      dropOperation:(NSTableViewDropOperation)inOperation
{
   // Look for our private type for reordering rows.
   NSString *type = [[inInfo draggingPasteboard]
      availableTypeFromArray:[NSArray
         arrayWithObjects:kPrivateRowPBType, nil]];
   if ([type isEqualToString:kPrivateRowPBType])
   {
      NSData *archivedRowData
         = [[inInfo draggingPasteboard]
            dataForType:kPrivateRowPBType];
      NSArray *rows = [NSUnarchiver
         unarchiveObjectWithData:archivedRowData];
      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)
         withObjectsFromArray:movedRows];
      // 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
      writeRows:(NSArray*)inRows
      toPasteboard:(NSPasteboard*)inPasteboard
{
   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]
      forType:NSStringPboardType];
   [inPasteboard setString:[NSString stringWithString:tabsBuf]
      forType:NSTabularTextPboardType];
   [inPasteboard setData: archivedRowData forType:
      kPrivateRowPBType];
   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

draggingSourceOperationMaskForLocal:
Allow drags outside of an application.
- (NSDragOperation)draggingSourceOperationMaskForLocal:
      (BOOL)isLocal
{
   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

copy:
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:
      @selector(tableView:writeRows:toPasteboard:)])
   {
      (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

selectedRows
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 dwood@karelia.com.

 
AAPL
$105.22
Apple Inc.
+0.39
MSFT
$46.13
Microsoft Corpora
+1.11
GOOG
$539.78
Google Inc.
-4.20

MacTech Search:
Community Search:

Software Updates via MacUpdate

Ember 1.8.2 - Versatile digital scrapboo...
Ember (formerly LittleSnapper) is your digital scrapbook of things that inspire you: websites, photos, apps or other things. Just drag in images that you want to keep, organize them into relevant... Read more
Tonality Pro 1.1.2 - Professional-grade...
Tonality Pro gives you the power to create stunning and dramatic black & white images. This is a complete monochrome image editor with more than 150 one-click style presets, totally unique... Read more
VueScan 9.4.49 - Scanner software with a...
VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more
OS X Server 4.0 - For OS X 10.10 Yosemit...
Designed for OS X and iOS devices, OS X Server makes it easy to share files, schedule meetings, synchronize contacts, develop software, host your own website, publish wikis, configure Mac, iPhone,... Read more
TotalFinder 1.6.12 - Adds tabs, hotkeys,...
TotalFinder is a universally acclaimed navigational companion for your Mac. Enhance your Mac's Finder with features so smart and convenient, you won't believe you ever lived without them. Tab-based... Read more
BusyCal 2.6.3 - Powerful calendar app wi...
BusyCal is an award-winning desktop calendar that combines personal productivity features for individuals with powerful calendar sharing capabilities for families and workgroups. BusyCal's unique... Read more
calibre 2.7 - Complete e-library managem...
Calibre is a complete e-book library manager. Organize your collection, convert your books to multiple formats, and sync with all of your devices. Let Calibre be your multi-tasking digital... Read more
Skitch 2.7.3 - Take screenshots, annotat...
With Skitch, taking, annotating, and sharing screenshots or images is as fun as it is simple.Communicate and collaborate with images using Skitch and its intuitive, engaging drawing and annotating... Read more
Delicious Library 3.3.2 - Import, browse...
Delicious Library allows you to import, browse, and share all your books, movies, music, and video games with Delicious Library. Run your very own library from your home or office using our... Read more
Art Text 2.4.8 - Create high quality hea...
Art Text is an OS X application for creating high quality textual graphics, headings, logos, icons, Web site elements, and buttons. Thanks to multi-layer support, creating complex graphics is no... Read more

Latest Forum Discussions

See All

Rami Ismail Opens Up distribute​() for D...
Rami Ismail Opens Up distribute​() for Developers Posted by Jessica Fisher on October 24th, 2014 [ permalink ] Rami Ismail, Chief Executive of Business and Development at indie game studio | Read more »
Great Hitman GO Goes on Sale and Gets Ne...
Great Hitman GO Goes on Sale and Gets New Update – Say That Three Times Fast Posted by Jessica Fisher on October 24th, 2014 [ permalink ] | Read more »
Rival Stars Basketball Review
Rival Stars Basketball Review By Jennifer Allen on October 24th, 2014 Our Rating: :: RESTRICTIVE BUT FUNUniversal App - Designed for iPhone and iPad Rival Stars Basketball is a fun mixture of basketball and card collecting but its... | Read more »
Rubicon Development Makes Over a Dozen o...
Rubicon Development Makes Over a Dozen of Their Games Free For This Weekend Only Posted by Jessica Fisher on October 24th, 2014 [ permalink ] | Read more »
I Am Dolphin Review
I Am Dolphin Review By Jennifer Allen on October 24th, 2014 Our Rating: :: NEARLY FIN-TASTICUniversal App - Designed for iPhone and iPad Swim around and eat nearly everything that moves in I Am Dolphin, a fun Ecco-ish kind of game... | Read more »
nPlayer looks to be the ultimate choice...
Developed by Newin Inc, nPlayer may seem like your standard video player – but is aiming to be the best in its field by providing high quality video play performance and support for a huge number of video formats and codecs. User reviews include... | Read more »
Fighting Fantasy: Caverns of the Snow Wi...
Fighting Fantasy: Caverns of the Snow Witch Review By Jennifer Allen on October 24th, 2014 Our Rating: :: CLASSY STORYTELLINGUniversal App - Designed for iPhone and iPad Fighting Fantasy: Caverns of the Snow Witch is a sterling... | Read more »
A Few Days Left (Games)
A Few Days Left 1.01 Device: iOS Universal Category: Games Price: $3.99, Version: 1.01 (iTunes) Description: Screenshots are in compliance to App Store's 4+ age rating! Please see App Preview for real game play! **Important: Make... | Read more »
Toca Boo (Education)
Toca Boo 1.0.2 Device: iOS Universal Category: Education Price: $2.99, Version: 1.0.2 (iTunes) Description: BOO! Did I scare you!? My name is Bonnie and my family loves to spook! Do you want to scare them back? Follow me and I'll... | Read more »
Intuon (Games)
Intuon 1.1 Device: iOS Universal Category: Games Price: $.99, Version: 1.1 (iTunes) Description: Join the battle with your intuition in a new hardcore game Intuon! How well do you trust your intuition? Can you find a needle in a... | Read more »

Price Scanner via MacPrices.net

Weekend sale: 13-inch 128GB MacBook Air for $...
Best Buy has the 2014 13-inch 1.4GHz 128GB MacBook Air on sale for $849.99, or $150 off MSRP, on their online store. Choose free home shipping or free local store pickup (if available). Price valid... Read more
Nimbus Note Cross=Platform Notes Utility
Nimbus Note will make sure you never forget or lose your valuable data again. Create and edit notes, save web pages, screenshots and any other type of data – and share it all with your friends and... Read more
NewerTech’s Snuglet Makes MagSafe 2 Power Con...
NewerTech has introduced the Snuglet, a precision-manufactured ring designed to sit inside your MagSafe 2 connector port, providing a more snug fit to prevent your power cable from unintentional... Read more
Apple Planning To Sacrifice Gross Margins To...
Digitimes Research’s Jim Hsiao says its analysts believe Apple is planning to sacrifice its gross margins to save its tablet business, which has recently fallen into decline. They project that Apple’... Read more
Who’s On Now? – First Instant-Connect Search...
It’s nighttime and your car has broken down on the side of the highway. You need a tow truck right away, so you open an app on your iPhone, search for the closest tow truck and send an instant... Read more
13-inch 2.5GHz MacBook Pro on sale for $949,...
Best Buy has the 13″ 2.5GHz MacBook Pro available for $949.99 on their online store. Choose free shipping or free instant local store pickup (if available). Their price is $150 off MSRP. Price is... Read more
Save up to $125 on Retina MacBook Pros
B&H Photo has the new 2014 13″ and 15″ Retina MacBook Pros on sale for up to $125 off MSRP. Shipping is free, and B&H charges NY sales tax only. They’ll also include free copies of Parallels... Read more
Apple refurbished Time Capsules available sta...
The Apple Store has certified refurbished Time Capsules available for up to $60 off MSRP. Apple’s one-year warranty is included with each Time Capsule, and shipping is free: - 2TB Time Capsule: $255... Read more
Textilus New Word, Notes and PDF Processor fo...
Textilus is new word-crunching, notes, and PDF processor designed exclusively for the iPad. I haven’t had time to thoroughly check it out yet, but it looks great and early reviews are positive.... Read more
WD My Passport Pro Bus-Powered Thunderbolt RA...
WD’s My Passport Pro RAID solution is powered by an integrated Thunderbolt cable for true portability and speeds as high as 233 MB/s. HighlightsOverviewSpecifications Transfer, Back Up And Edit In... Read more

Jobs Board

*Apple* Solutions Consultant - Apple Inc. (U...
…important role that the ASC serves is that of providing an excellent Apple Customer Experience. Responsibilities include: * Promoting Apple products and solutions Read more
Senior Event Manager, *Apple* Retail Market...
…This senior level position is responsible for leading and imagining the Apple Retail Team's global event strategy. Delivering an overarching brand story; in-store, Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Project Manager / Business Analyst, WW *Appl...
…a senior project manager / business analyst to work within our Worldwide Apple Fulfillment Operations and the Business Process Re-engineering team. This role will work Read more
*Apple* Retail - Multiple Positions (US) - A...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.