TweetFollow Us on Twitter

Takes All Sorts

Volume Number: 18 (2002)
Issue Number: 8
Column Tag: Mac OS X

Takes All Sorts

Make Your TableViews Autosort!

by Andrew C. Stone

One of the coolest pieces of object technology in the Cocoa bag of tricks is the NSTableView, and its daughter, NSOutlineView. The tableView lets you display a list of items with various properties. As in a spreadsheet, it displays rows of information - anything from simple text to graphics or even quicktime movies! Once you learn the fundamentals of the tableView and the outlineView, they'll quickly become some of your favorite user interface objects.

This article will take the NSTableView further, and show you how to add "auto sort" to your tableView, and the objects it represents. It assumes you have read the documentation for NSTableView and its companion class NSTableColumn and perhaps already have an app with an NSTableView of your own. You can try out this sort idea in Apple's Mail. A Mail Box shows a tableView of the list of messages, sender, subject, date, size, etc:

Figure 1. You can play with quick sort feature in Apple's Mail application.

When you click on the title cell of a column, an arrow appears to show direction of the sort, the cell is highlighted, and the messages are sorted on that key. If you click the title cell of that same column again, it toggles the sort to reverse its direction: if it was an ascending sort, now it's a descending sort. Sorting mail by subject is particularly interesting for deleting spam en masse!

When Time Equals Money...

When preparing TimeEqualsMoney 2.0 <> this spring, I decided users would love to be able to sort their time and expense entries on any key, in either direction with a simple click. So I reread the documentation Apple provides on the NSTableView class. I highly recommend actually reading the .h files! It's amazing how much stuff is already anticipated for you! There is a delegate method, - (void) tableView:(NSTableView*)tv didClickTableColumn:(NSTableColumn *)tableColumn. So, it seemed, I simply had to implement this method in my NSWindowController subclass, which is architecturally a sound choice for the NSTableView delegate. However, the method was simply not being called when I clicked on the column header cell. It turns out, and I noted this heavily in my code, that if neither column reodering nor column selection is selected in Interface Builder's NSTableView inspector, then the method is not called. Turning on column reordering was a simple fix and a desired feature! You can, of course, also call one of these methods programmatically with a parameter of YES:

- (void)setAllowsColumnReordering:(BOOL)flag;
- (void)setAllowsColumnSelection:(BOOL)flag;

Following on the Model-View-Controller pattern that I have discussed in previous articles on Cocoa architecture, I decided to store the sort column, and its direction in my NSDocument subclass, as well as make it responsible for the sorting of its items. This way, you can undo the sort, automatically mark the document as unsaved as well as save the sort between sessions. The user clicks the tableView column header, the window controller then sets the document's sort key, or direction if the key was already set to that column. Then the document tells itself to sort, which tells its NSMutableArray of entries to sort its contents. Finally, the document tells its window controllers to reload the tableView, which updates the interface to display the new sort order:

Figure 2. This tableView lets you click a column header to sort on that key - up or down.

Model Behavior

The implementation of the actual sort is trivial: just ask the list to sort itself based on asking its elements to sort using a selector: [list sortUsingSelector:@selector(compare:)];

During the list's sorting procedure, it repeatedly calls, in this case, compare: on one list item against another.

I like to just reuse the Kit's compare: which is implemented in NSString, NSDate, NSCell, NSValue, etc:

- (NSComparisonResult)compare:(id)other;

The NSComparisonResult has these three values: NSOrderedAscending = -1, NSOrderedSame = 0, NSOrderedDescending = 1. Since so many objects reply to compare:, you might implement it something like this in your data object, given that the sort key is the same string as the column identifiers, and you have various numbers, dates, strings and even booleans:

// the Data MODEL - the document has a list of these entities:
@interface PieceWork : NSObject <NSCopying>
    NSCalendarDate *startTime;
    NSString *description;
    NSTimeInterval timeOnJob;
    float _rate;
    BOOL paid;
    id owner;
- (NSComparisonResult)compare:(id)other;
- (void)setOwner:(id)ownsMe;
- (NSCalendarDate *)startTime;
- (NSTimeInterval)timeOnJob;
- (NSString *)workDescription;
- (float)hoursOnJob;
- (float)rate;
- (BOOL)paid;
@implementation PieceWork
- (NSComparisonResult)compare:(id)other {
    BOOL isDescending = [owner sortIsDescending];
    NSString *key = [owner sortColumn];
   // now determine which one is first based on sort direction:
    PieceWork *first = isDescending ? other : self;
    if (first == other) other = self;
    if ([key isEqualToString:@"TimeOnJob"]) {
        return [[NSNumber numberWithFloat:[other timeOnJob]] compare: 
        [NSNumber numberWithFloat:[first timeOnJob]]];
    } else if ([key isEqualToString:@"Date"]) {
        return [[other startTime] compare: [first startTime]];
    } else if ([key isEqualToString:@"Description"]) {
// Very handy macro:
#define IS_NULL(s) (!s || [s isEqualToString:@""])
            if (IS_NULL([other workDescription])) return NSOrderedDescending;
            if (IS_NULL([first workDescription])) return NSOrderedAscending;
// people prefer upper and lower case mixed, unlike UNIX:
        return [[other workDescription] caseInsensitiveCompare:[first workDescription]];
    } else if ([key isEqualToString:@"Paid"]) {
        return [[NSNumber numberWithBool:[other paid]] compare:[NSNumber 
        numberWithBool:[first paid]]];
    return 0;

Here are the relevant instant variables and methods in the NSDocument subclass:

@interface MyDocument : NSDocument {
    NSMutableArray *_workList;
    NSString *_sortColumn;
    BOOL _sortIsDescending;
.   ...
- (void)sort;
- (NSArray *)workList;
- (void)setSortColumn:(NSString *)identifier;
- (NSString *)sortColumn;
- (void)setSortIsDescending:(BOOL)whichWay;
- (BOOL)sortIsDescending;
@implementation MyDocument
- (void)sort:(NSMutableArray *)list {
    [list makeObjectsPerformSelector:@selector(setOwner:) withObject:self];
    [list sortUsingSelector:@selector(compare:)];  // asks us for how!!
- (void) sort {
    if (NOT_NULL(_sortColumn)) {
        [self sort:_workList];
        [[self windowControllers] makeObjectsPerformSelector:@selector(updateTotalsAndReload)];
- (void)setSortColumn:(NSString *)identifier {
    if (![identifier isEqualToString:_sortColumn]) {
        [[[self undoManager] prepareWithInvocationTarget:self] setSortColumn:_sortColumn];
    [_sortColumn release];
    _sortColumn = [identifier copyWithZone:[self zone]];
        [[self undoManager] setActionName:NSLocalizedStringFromTable(@"Sort",@"TimeCard",@"title of 
        undo the sort action")];
- (NSString *)sortColumn {
    return _sortColumn;
- (void)setSortIsDescending:(BOOL)whichWay {
    if (whichWay != _sortIsDescending) {
        [[[self undoManager] prepareWithInvocationTarget:self] setSortIsDescending:_sortIsDescending];
    _sortIsDescending = whichWay;
        [[self undoManager] setActionName:NSLocalizedStringFromTable(@"Sort 
        Direction",@"TimeCard",@"title of undo the sort up or down action")];
- (BOOL)sortIsDescending {
    return _sortIsDescending;
- (NSArray *)workList { return _workList; }
- (void)setWorkList:(NSMutableArray *)list {
    [[[self undoManager] prepareWithInvocationTarget:self] setWorkList:_workList];
    [_workList release];
    if (NOT_NULL(_sortColumn)) [self sort:list];
    _workList = [list mutableCopyWithZone:[self zone]];
    [_workList makeObjectsPerformSelector:@selector(setOwner:) withObject:self];
    [[self windowControllers] makeObjectsPerformSelector:@selector(updateTotalsAndReload)];
    [[self undoManager] setActionName:NSLocalizedStringFromTable(@"Work Change", @"TimeCard", 
    @"Action name for changing worklist")];
// And don't forget to actually archive and read the sort info
// I highly recommended using human readable XML dictionaries:
- (NSMutableDictionary *)workDocumentDictionary {
    NSMutableDictionary *doc = [NSMutableDictionary dictionary];
        unsigned i, c = [_workList count];
        if (NOT_NULL(_sortColumn)) [doc setObject:_sortColumn forKey:SortNameKey];
        if (_sortIsDescending) [doc setObject:@"YES" forKey:SortDescendingKey];
       return doc;
- (BOOL)loadDataRepresentation:(NSData *)data ofType:
                                                      (NSString *)type {
    if ([type isEqualToString:DocumentType]) {
 NSDictionary *doc = [self workDocumentDictionaryFromData:data];
        id obj;
        obj = [doc objectForKey:SortNameKey];
        if (obj) {
            [self setSortColumn:obj];
      obj = [doc objectForKey:SortDescendingKey];
        if (obj) {
            _sortIsDescending = [@"YES" isEqualToString:obj];
        return YES;
  return NO;

Control Freak

Finally, we come to the meat of the matter in our NSWindowController subclass which is our NSTableView's dataSource as well as delegate.

@interface JobWindowController : NSWindowController 
    IBOutlet NSTableView *tableView;
    NSImage *descendingSortingImage;
    NSImage *ascendingSortingImage;

We have work to do in our window initialization method that gets called when the Interface has loaded, awakeFromNib. We want to get the images that we shall display when a sort is made. Using class-dump as described in my iPhoto Exporter bundle article <>, you might note that there are hidden, private Apple internal API to access the images to indicate ascending or descending sorts! Since one should absolutely never rely on hidden API to not dissolve into the ether in subsequent system releases, always bracket the use of these methods with a "respondsToSelector:" query, and provide backup images of your own:

- (void)awakeFromNib
    // private images:
    if ([[NSTableView class] respondsToSelector:@selector(_defaultTableHeaderSortImage)])
    ascendingSortingImage = [[NSTableView class] _defaultTableHeaderSortImage];
    else ascendingSortingImage = [[NSImage imageNamed:@"ascendingSort"] retain];
    if ([[NSTableView class] respondsToSelector:@selector(_defaultTableHeaderReverseSortImage)])
    descendingSortingImage = [[NSTableView class] _defaultTableHeaderReverseSortImage];
    else descendingSortingImage = [[NSImage imageNamed:@"descendingSort"] retain];

If you allow users to move and resize columns, then you definitely want to additionaly call these two methods in awakeFromNib - this will reestablish their preferences and give the system a name to save the column order and sizes.

    [tableView setAutosaveTableColumns:YES];
    [tableView setAutosaveName:@"WorkTable"];

So, now we're ready to implement the delegate method which gets called when the column title cell is clicked. The one fine point is that we want to make a note of which item was selected before the sort, so we can reselect it after the sort (its row number may have changed after the sort). We ask the tableView if it already has an image in that column, thus indicating that we need to toggle the direction, otherwise, we'll set the new sort key. The actual work of setting up the tableView's header hilighting is done in updateTableHeaderToMatchCurrentSort. This is factored into a separate method so we can also call it when loading a document to restore last saved sort state.

// BIG COCOA NOTE: if column reordering or column selection is not on
// then this doesn't get called!!
- (void) tableView:(NSTableView*)tv didClickTableColumn:(NSTableColumn *)tableColumn {
    NSImage *sortOrderImage = [tv indicatorImageInTableColumn:tableColumn];
    NSString *columnKey = [tableColumn identifier];
    MyDocument *doc = [self document];
    PieceWork *work = [self selectedWork];
    // If the user clicked the column which already has the sort indicator
    // then just flip the sort order.
    if (sortOrderImage || columnKey == [doc sortColumn]) {
        [doc setSortIsDescending:![doc sortIsDescending]]; 
    } else {
        [doc setSortColumn:columnKey];
    [self updateTableHeaderToMatchCurrentSort];
    // now do it - doc calls us back when done
    [doc sort];
    // but reselect the one previously selected:
    if (work) [self selectWork:work];

This method updates the state of the NSTableColumn by first clearing all cells, and then setting the correct image and hilighting on the column to sort by, if any:

- (void)updateTableHeaderToMatchCurrentSort {
    BOOL isDescending = [[self document] sortIsDescending];
    NSString *key = [[self document] sortColumn];
    NSArray *a = [tableView tableColumns];
    NSTableColumn *column = [tableView tableColumnWithIdentifier:key];
    unsigned i = [a count];
    while (i-- > 0) [tableView setIndicatorImage:nil inTableColumn:[a objectAtIndex:i]];
    if (NOT_NULL(key)) {
        [tableView setIndicatorImage:(isDescending ? ascendingSortingImage:descendingSortingImage) 
        [tableView setHighlightedTableColumn:column];
    } else [tableView setHighlightedTableColumn:nil];

And our helper functions to make the code neat and compact:

- (NSArray *)workList {
   return [[self document] workList];
- (void)updateTotalsAndReload {
// other ui:
//    [self updateTotals];
    [tableView reloadData];
- (PieceWork *)selectedWork {
   NSArray *workList = [self workList];
   int row = [tableView selectedRow];
   if (row > -1 && row < [workList count]) return [workList objectAtIndex:row];
   return nil;
- (void)selectWork:(PieceWork *)work {
    NSArray *a = [self workList];
    unsigned i = [a indexOfObject:work];
    if (i != NSNotFound) [self selectRow:i];

Devil in the Details

So, now we got sorting working, what sort of problems might we run into? Let's say a user edits the field of an entry in the sorted column. Upon finishing editing, they may have changed the relative order of that entry, requiring a new sort, and a reloading of the tableView again! We can check if we have the sorted column when the data gets set in - tableView:setObjectValue:forTableColumn:row:

- (void)tableView:(NSTableView *)tv setObjectValue:(id)object 
forTableColumn:(NSTableColumn *)tc row:(int)row;
        NSArray *workList = [self workList];
        NSString *ident = [tc identifier];
        if (tv == tableView) {
            PieceWork *work = [workList objectAtIndex:row];
            if ([ident isEqual:@"Description"]) {
                [work setWorkDescription:object];
                [self sortIfSortedOn:ident work:work];
           } else if ....

Isn't the devil in the details? Because the tableView will want to "select next" after setObjectValue: is called, the wrong row might be selected if the sort changed the currently selected item's row. By calling this method, we'll first end editing on the tableView so any inadvertent select next is foiled. Note the delay of 0.0 seconds which means to schedule the call of the method after the current event loop finishes:

- (void)afterSort:(PieceWork *)work {
    [[self window] makeFirstResponder:[self window]];
    [self selectWork:work];
- (void)sortIfSortedOn:(NSString *)ident work:(PieceWork *)work {
    if ([[[self document] sortColumn] isEqualToString:ident])  {
        int oldPosition
        [[self window] makeFirstResponder:[self window]];
        [[self document] sort];
        [self performSelector:@selector(afterSort:) withObject:work afterDelay:0.0];
    } else [self updateTotalsAndReload];


Sorting is something users expect tableViews to do now - and with a little effort, all of your tableViews can sort themselves!

Andrew Stone,, is Chief Chef at Stone Design,, has this to say about Cocoa and Stone Studio with all due respect to Gary Snyder's Turtle Island, "It's objects all the way down" .


Community Search:
MacTech Search:

Software Updates via MacUpdate

The best games like Animal Crossing on m...
Animal Crossing amiibo Festival is out right now for the Wii U, reminding us of just how much fun that world can be. Or at least to go back and check in on our villages once in a while. [Read more] | Read more »
Between 2 Taps - Tap for Tap interview M...
Hello, and welcome back to Between 2 Taps, Tap for Tap’s Indie Dev interview series. [Read more] | Read more »
Facility 47 (Games)
Facility 47 1.0.1 Device: iOS Universal Category: Games Price: $3.99, Version: 1.0.1 (iTunes) Description: You wake up alone and freezing in an icy cell. You try the cell door but it’s locked, it seems that you are stuck with no... | Read more »
The best Photoshop alternative on iPad
Instagram and Lightroom are great and all, but sometimes people need to get extra creative with their image editing.Like, Photoshop creative. If you're one of these people, take a look at our pick for the best mobile Photoshop experience on iPad... | Read more »
The Walking Dead: No Man’s Land guide -...
A new update for The Walking Dead: No Man’s Land was released last week, making it the perfect time for you to head back to your base and take out some walkers. Here’s the lowdown on what’s new to the game, and how to take advantage. [Read more] | Read more »
Goat Rider guide - Tips and tricks to st...
We've all been there. One second, we're riding high on a crazed goat, and the next, we've been tossed off it like someone who's no good at goat ridin'. [Read more] | Read more »
Real Boxing 2 CREED: How to become a gre...
Just in time for Rocky fans who can’t wait to see CREED, the latest movie, we have the official tie-in game,Real Boxing 2 CREED. It builds on the success of its predecessor and there’s lots to take in so we at 148apps thought we’d run you through... | Read more »
CoinOp Heroes 2 guide - How to build an...
CoinOp Heroes 2 justlaunched and, like all clickers, it's dangerously addictive stuff. You have to furiously tap your screen to defeat wave after wave of foes and earn an insane amount of cash to spend on character upgrades and an army of minions... | Read more »
Dr. Panda Firefighters (Education)
Dr. Panda Firefighters 1.0.1 Device: iOS Universal Category: Education Price: $2.99, Version: 1.0.1 (iTunes) Description: FIGHT FIRES AND SAVE THE DAY!Work together with Dr. Panda and his firefighting team to rescue his trapped... | Read more »
Puddle + (Games)
Puddle + 1.0 Device: iOS iPhone Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: Puddle is back in a new "+" edition featuring enhanced graphics, new videos and Apple TV support ! No IAP and No Ads. Dive into Puddle... | Read more »

Price Scanner via

Save $30-$40 on new Apple TVs after rebate
Adorama has new Apple TVs on sale for up to $40 off MSRP after mail-in rebate, good through December 15th. Shipping is free, and Adorama charges NY & NJ sales tax only: - 32GB Apple TV: $119.99... Read more
13-Inch Haswell MacBook Air At Two Years – Th...
The 13-inch mid-2013 “Haswell” MacBook Air I ordered in Apple’s November 2013 Black Friday sale was my first new Mac in four and a half years — the longest interval I’ve gone between system upgrades... Read more
Target Black Friday Early Access deals: $100...
Target is offering early access to their Black Friday deals on Apple products on their online store for today, the 25th, only. Choose free shipping or free local store pickup (if available): - Apple... Read more
BlackBerry Q3 Mobility Index Report Finds iOS...
BlackBerry has announced results of its thirteenth Good Mobility Index Report, showing that organizations are increasingly building custom secure apps. Among Good Powered by BlackBerry (formerly Good... Read more
Wednesday roundup of early Black Friday Mac s...
Save up to $500 on a new Mac with these early Black Friday deals from Apple resellers, currently the lowest prices available for these models: (1) B&H Photo has all new Macs on sale for up to $... Read more
iPod nano on sale for $119, $30 off MSRP
Walmart has the 16GB iPod nano (various colors) on sale for $119.20 on their online store for a limited time. That’s $30 off MSRP. Choose free shipping or free local store pickup (if available). Sale... Read more
Adorama Black Friday deals: Up to $400 off Ma...
Adorama has released their Black Friday deals for 2015. Save up to $400 on MacBook Pros, $200 on MacBooks and MacBook Airs, and $270 on iMacs. Use code RYBFDEAL during checkout to see these prices.... Read more
B&H Photo Deals: $200 off 12-inch 1.2GHz...
In addition to the B&H Photo Black Friday week sales we posted yesterday, B&H has lowered their price on two products to $200 off MSRP: - 12″ 1.2GHz Gray Retina MacBook: $1399 save $200 - 13... Read more
Best Buy Early Access: Today only, Up to $125...
Best Buy has iPad Air 2s on sale for up to $125 off MSRP and Apple Watch models on sale on their online store for up to $100 off MSRP with special codes through midnight CT tonight. Choose free... Read more
UPPERCASE DESIGNS Premium Ultra Thin Keyboard...
UPPERCASE Designs today announced its new Premium Ultra Thin Keyboard Protector and its Palm Rest Protector Set for the 12-inch MacBook. The accessories provide durable protection for the 12-inch... Read more

Jobs Board

Hardware Systems Integration Engineer - *App...
# Hardware Systems Integration Engineer - Apple Watch Job Number: 39380139 Santa Clara Valley, Califo ia, United States Posted: Apr. 23, 2015 Weekly Hours: **Job Read more
Sr. Technical/Project Manager, *Apple* Educ...
# Sr. Technical/Project Manager, Apple Education Job Number: 36588557 New York City, New York, United States Posted: Jul. 30, 2015 Weekly Hours: 40.00 **Job Summary** 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
Merchant Operations Manager: *Apple* Pay -...
Changing the world is all in a day's work at Apple . If you love innovation, here's your chance to make a career of it. You'll work hard. But the job comes with more than Read more
*Apple* Pay QA Manager - Apple Inc. (United...
Changing the world is all in a day's work at Apple . If you love innovation, here's your chance to make a career of it. You'll work hard. But the job comes with more than Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.