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.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Hazel 5.3 - Create rules for organizing...
Hazel is your personal housekeeper, organizing and cleaning folders based on rules you define. Hazel can also manage your trash and uninstall your applications. Organize your files using a familiar... Read more
Duet 3.15.0.0 - Use your iPad as an exte...
Duet is the first app that allows you to use your iDevice as an extra display for your Mac using the Lightning or 30-pin cable. Note: This app requires a iOS companion app. Release notes were... Read more
DiskCatalogMaker 9.0.3 - Catalog your di...
DiskCatalogMaker is a simple disk management tool which catalogs disks. Simple, light-weight, and fast Finder-like intuitive look and feel Super-fast search algorithm Can compress catalog data for... Read more
Maintenance 3.1.2 - System maintenance u...
Maintenance is a system maintenance and cleaning utility. It allows you to run miscellaneous tasks of system maintenance: Check the the structure of the disk Repair permissions Run periodic scripts... Read more
Final Cut Pro 10.7 - Professional video...
Redesigned from the ground up, Final Cut Pro combines revolutionary video editing with a powerful media organization and incredible performance to let you create at the speed of thought.... Read more
Pro Video Formats 2.3 - Updates for prof...
The Pro Video Formats package provides support for the following codecs that are used in professional video workflows: Apple ProRes RAW and ProRes RAW HQ Apple Intermediate Codec Avid DNxHD® / Avid... Read more
Apple Safari 17.1.2 - Apple's Web b...
Apple Safari is Apple's web browser that comes bundled with the most recent macOS. Safari is faster and more energy efficient than other browsers, so sites are more responsive and your notebook... Read more
LaunchBar 6.18.5 - Powerful file/URL/ema...
LaunchBar is an award-winning productivity utility that offers an amazingly intuitive and efficient way to search and access any kind of information stored on your computer or on the Web. It provides... Read more
Affinity Designer 2.3.0 - Vector graphic...
Affinity Designer is an incredibly accurate vector illustrator that feels fast and at home in the hands of creative professionals. It intuitively combines rock solid and crisp vector art with... Read more
Affinity Photo 2.3.0 - Digital editing f...
Affinity Photo - redefines the boundaries for professional photo editing software for the Mac. With a meticulous focus on workflow it offers sophisticated tools for enhancing, editing and retouching... Read more

Latest Forum Discussions

See All

New ‘Sonic Dream Team’ Performance Analy...
Sonic Dream Team (), the brand new Apple Arcade exclusive 3D Sonic action-platformer releases tomorrow. Read my in-depth interview with SEGA HARDLight here covering the game, future plans, potential 120fps, playable characters, the narrative, and... | Read more »
New ‘Zenless Zone Zero’ Trailer Showcase...
I missed this late last week, but HoYoverse released another playable character trailer for the upcoming urban fantasy action RPG Zenless Zone Zero. We’ve had a few trailers so far for playable characters, but the newest one focuses on Nicole... | Read more »
Get your heart pounding quicker as Autom...
When it comes to mobile battle royales, it wouldn’t be disrespectful to say the first ones to pop to mind would be PUBG Mobile, but there is one that perhaps doesn’t get the worldwide recognition it should and that's Garena’s Free Fire. Now,... | Read more »
‘Disney Dreamlight Valley Arcade Edition...
Disney Dreamlight Valley Arcade Edition () launches beginning Tuesday worldwide on Apple Arcade bringing a new version of the game without any passes or microtransactions. While that itself is a huge bonus for Apple Arcade, the fact that Disney... | Read more »
Adventure Game ‘Urban Legend Hunters 2:...
Over the weekend, publisher Playism announced a localization of the Toii Games-developed adventure game Urban Legend Hunters 2: Double for iOS, Android, and Steam. Urban Legend Hunters 2: Double is available on mobile already without English and... | Read more »
‘Stardew Valley’ Creator Has Made a Ton...
Stardew Valley ($4.99) game creator Eric Barone (ConcernedApe) has been posting about the upcoming major Stardew Valley 1.6 update on Twitter with reveals for new features, content, and more. Over the weekend, Eric Tweeted that a ton of progress... | Read more »
Pour One Out for Black Friday – The Touc...
After taking Thanksgiving week off we’re back with another action-packed episode of The TouchArcade Show! Well, maybe not quite action-packed, but certainly discussion-packed! The topics might sound familiar to you: The new Steam Deck OLED, the... | Read more »
TouchArcade Game of the Week: ‘Hitman: B...
Nowadays, with where I’m at in my life with a family and plenty of responsibilities outside of gaming, I kind of appreciate the smaller-scale mobile games a bit more since more of my “serious" gaming is now done on a Steam Deck or Nintendo Switch.... | Read more »
SwitchArcade Round-Up: ‘Batman: Arkham T...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for December 1st, 2023. We’ve got a lot of big games hitting today, new DLC For Samba de Amigo, and this is probably going to be the last day this year with so many heavy hitters. I... | Read more »
Steam Deck Weekly: Tales of Arise Beyond...
Last week, there was a ton of Steam Deck coverage over here focused on the Steam Deck OLED. | Read more »

Price Scanner via MacPrices.net

Apple has Certified Refurbished iPhone 12 Pro...
Apple has unlocked Certified Refurbished iPhone 12 Pro models in stock starting at $589 and ranging up to $350 off original MSRP. Apple includes a standard one-year warranty and new outer shell with... Read more
Holiday Sale: Take $50 off every 10th-generat...
Amazon has Apple’s 10th-generation iPads on sale for $50 off MSRP, starting at $399, as part of their Holiday Sale. Their discount applies to all models and all colors. With the discount, Amazon’s... Read more
The latest Mac mini Holiday sales, get one to...
Apple retailers are offering Apple’s M2 Mac minis for $100 off MSRP as part of their Holiday sales. Prices start at only $499. Here are the lowest prices available: (1): Amazon has Apple’s M2-powered... Read more
Save $300 on a 24-inch iMac with these Certif...
With the recent introduction of new M3-powered 24″ iMacs, Apple dropped prices on clearance M1 iMacs in their Certified Refurbished store. Models are available starting at $1049 and range up to $300... Read more
Apple M1-powered iPad Airs are back on Holida...
Amazon has 10.9″ M1 WiFi iPad Airs back on Holiday sale for $100 off Apple’s MSRP, with prices starting at $499. Each includes free shipping. Their prices are the lowest available among the Apple... Read more
Sunday Sale: Apple 14-inch M3 MacBook Pro on...
B&H Photo has new 14″ M3 MacBook Pros, in Space Gray, on Holiday sale for $150 off MSRP, only $1449. B&H offers free 1-2 day delivery to most US addresses: – 14″ 8-Core M3 MacBook Pro (8GB... Read more
Blue 10th-generation Apple iPad on Holiday sa...
Amazon has Apple’s 10th-generation WiFi iPad (in Blue) on Holiday sale for $349 including free shipping. Their discount applies to WiFi models only and includes a $50 instant discount + $50 clippable... Read more
All Apple Pencils are on Holiday sale for $79...
Amazon has all Apple Pencils on Holiday sale this weekend for $79, ranging up to 39% off MSRP for some models. Shipping is free: – Apple Pencil 1: $79 $20 off MSRP (20%) – Apple Pencil 2: $79 $50 off... Read more
Deal Alert! Apple Smart Folio Keyboard for iP...
Apple iPad Smart Keyboard Folio prices are on Holiday sale for only $79 at Amazon, or 50% off MSRP: – iPad Smart Folio Keyboard for iPad (7th-9th gen)/iPad Air (3rd gen): $79 $79 (50%) off MSRP This... Read more
Apple Watch Series 9 models are now on Holida...
Walmart has Apple Watch Series 9 models now on Holiday sale for $70 off MSRP on their online store. Sale prices available for online orders only, in-store prices may vary. Order online, and choose... Read more

Jobs Board

Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Senior Product Manager - *Apple* - DISH Net...
…Responsibilities** We are seeking an ambitious, data-driven thinker to assist the Apple Product Development team as our Wireless Product division continues to grow Read more
Senior Product Manager - *Apple* - DISH Net...
…Responsibilities** We are seeking an ambitious, data-driven thinker to assist the Apple Product Development team as our Wireless Product division continues to grow Read more
Senior Software Engineer - *Apple* Fundamen...
…center of Microsoft's efforts to empower our users to do more. The Apple Fundamentals team focused on defining and improving the end-to-end developer experience in Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.