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
$101.79
Apple Inc.
+0.21
MSFT
$46.68
Microsoft Corpora
+0.16
GOOG
$589.27
Google Inc.
+4.50

MacTech Search:
Community Search:

Software Updates via MacUpdate

iMazing 1.0 - Complete iOS device manage...
iMazing (formerly DiskAid) is the ultimate iOS device manager with capabilities far beyond what iTunes offers. With iMazing and your iOS device (iPhone, iPad, or iPod), you can: Copy music to and... Read more
Xcode 6.0.1 - Integrated development env...
Apple Xcode is Apple Computer's integrated development environment (IDE) for OS X. The full Xcode package is free to ADC members and includes all the tools you need to create, debug, and optimize... Read more
Apple Safari 7.1 - Apple's Web brow...
Apple Safari in OS X Mavericks brings you all-new ways to find and enjoy the best of the web. It works with iCloud to give you a seamless browsing experience across all your devices. It looks out for... Read more
Delivery Status 6.1.2 - Check delivery s...
Delivery Status displays delivery status of packages for a variety of shipment services. Can't wait for your packages to arrive? Don't waste your time checking the site constantly, just open this all... Read more
Mavericks Cache Cleaner 8.0.9 - Clear ca...
Mavericks Cache Cleaner is an award-winning general purpose tool for OS X. MCC makes system maintenance simple with an easy point-and-click interface to many OS X functions. Novice and expert users... Read more
OneNote 15.2.2 - Free digital notebook f...
OneNote is your very own digital notebook. With OneNote, you can capture that flash of genius, that moment of inspiration, or that list of errands that's too important to forget. Whether you're at... Read more
Apple Configurator 1.6 - Configure and d...
Apple Configurator makes it easy for anyone to mass configure and deploy iPhone, iPad, and iPod touch in a school, business, or institution. Three simple workflows let you prepare new iOS devices... Read more
SpamSieve 2.9.16 - Robust spam filter fo...
SpamSieve is a robust spam filter for major email clients that uses powerful Bayesian spam filtering. SpamSieve understands what your spam looks like in order to block it all, but also learns what... Read more
OS X Server 3.2.1 - For OS X 10.9.5 Mave...
OS X Server is the next generation of Apple's award winning server software. Designed for OS X and iOS devices, OS X Server makes it easy to share files, schedule meetings, synchronize contacts, host... Read more
Apple Security Update 2014-004 - For OS...
Apple Security Update is recommended for all users and improves the security of Mac OS X. For information on the security content of this update, please visit this website: http://support.apple.com/... Read more

Latest Forum Discussions

See All

Down Among the Dead Men (Games)
Down Among the Dead Men 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: Avast! Take to the high seas in a fully interactive piratical tale of broadsides and buccaneers. From author Dave... | Read more »
Sling Adds Chromecast Support Through Sl...
Sling Adds Chromecast Support Through Slingplaye​r Mobile Apps Posted by Jessica Fisher on September 18th, 2014 [ permalink ] | Read more »
How to Completely Delete Your iPhone’s C...
The iPhone 6 is out tomorrow, and plenty of people are excited about it. So much so that they’re planning to – or already have – traded in their old iPhone to go towards it. The thing about trading in hardware is it’s very important to make sure... | Read more »
Dragon Quest I Review
Dragon Quest I Review By Andrew Fisher on September 18th, 2014 Our Rating: :: THINE QUEST AWAITETHUniversal App - Designed for iPhone and iPad Its historical significance aside, Dragon Quest 1 is a fun, campy, difficult, thoroughly... | Read more »
It Came From Canada: Overkill 3
Overkill 3 is like every trope of big modern gaming rolled into one. It’s a sequel to an action-packed military shooter. It’s flashy and scripted and flaunts its sophisticated graphics. And it’s a mobile game with a heavy emphasis on in-app... | Read more »
New Modes and Leader Boards in Update fo...
New Modes and Leader Boards in Update for Rules! Posted by Jessica Fisher on September 18th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
TwistedRun Review
TwistedRun Review By Rob Thomas on September 18th, 2014 Our Rating: :: DON'T TWIST YOUR ANKLE!Universal App - Designed for iPhone and iPad TwistedRun is kind of like running up a giant curly fry into the sky. Or maybe that was just... | Read more »
Scope Review
Scope Review By Jennifer Allen on September 18th, 2014 Our Rating: :: LOCATION AWAREiPhone App - Designed for the iPhone, compatible with the iPad Want to easily find photos from around the world based on their location? Scope is a... | Read more »
HipstaFox Review
HipstaFox Review By Jordan Minor on September 18th, 2014 Our Rating: :: FANTASTIC MR. FOXUniversal App - Designed for iPhone and iPad HipstaFox is a great single that makes players long for the whole album.   | Read more »
Ninja Raft (Games)
Ninja Raft 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: ** Special Launch Price ** "Ninja Raft is definitely the game to play if you’re into Tower Defense games and want to play something... | Read more »

Price Scanner via MacPrices.net

Save up to $300 on the price of a new Mac wit...
Purchase a new Mac or iPad at The Apple Store for Education and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free,... Read more
13-inch 2.8GHz Retina MacBook Pro available f...
B&H Photo has the new 2014 13″ 2.8GHz Retina MacBook Pro on sale for $1699.99 including free shipping plus NY sales tax only. They’ll also include free copies of Parallels Desktop and LoJack for... Read more
16GB iPad Air on sale for $449, save $50
Walmart has the 16GB iPad Air WiFi on sale for $449 on their online store for a limited time. Choose free home shipping or free local store pickup. Their price represents a $50 savings over standard... Read more
13-inch 256GB MacBook Air on sale for $1099,...
B&H Photo has the 2014 13″ 1.4GHz 256GB MacBook Air on sale for $1099.99. Shipping is free, and B&H charges NY sales tax only. Their price is $100 off MSRP. Read more
Toshiba Introduces TransMemory ID High-Speed...
Toshiba’s Digital Products Division (DPD), a division of Toshiba America Information Systems, Inc., today introduced the TransMemory ID USB 3.0 Flash Drive, a simpler storage solution for people who... Read more
New iPads and OS X Yosemite Release Coming Oc...
The DailyDot’s Micah Singleton reports that Apple is planning to hold its next product announcement event on Oct. 21, at which it will unveil the iPad Air 2 and iPad mini 3 and release a final build... Read more
Logitech Bluetooth Multi-Device Cross-Platfor...
Logitech has an enviable track record of making some of the best computer keyboards and mice. At least in my estimation, the best freestanding keyboards I’ve ever used have been Logitech units,... Read more
Roundup of Apple refurbished iPad Airs and iP...
Apple is offering Certified Refurbished iPad Airs for up to $140 off MSRP. Apple’s one-year warranty is included with each model, and shipping is free. Stock tends to come and go with some of these... Read more
Sprint offers 16GB iPad mini for $199.99 with...
Sprint is offering 1st generation 16GB iPad minis for $199.99 with a 2-year service agreement. Standard MSRP for this iPad is $429. Their price is the lowest available for this model. Read more
2.5GHz Mac mini remains on sale for $549, sav...
B&H Photo has the 2.5GHz Mac mini on sale for $549.99 including free shipping. That’s $50 off MSRP, and B&H will also include a free copy of Parallels Desktop software. NY sales tax only. Read more

Jobs Board

Project Manager, *Apple* Financial Services...
**Job Summary** Apple Financial Services (AFS) offers consumers, businesses and educational institutions ways to finance Apple purchases. We work with national and Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.