Fat Cat Software

Subclassing NSSortDescriptor for fun and profit

In the project I'm currently working on, I'm using a pretty standard NSTableView + NSArrayController setup using the default sorting functionality. However, I found myself wanting to do some custom sorting, i.e. not just the usual sorting provided by @selector(compare:). This is made a little more complex by the fact that the columns of the table can be dynamically added and removed, so it's not quite as simple as just changing the sort selector in Interface Builder. On top of that, I later found that I needed more customization still, because I needed to do special handling of nil values that was different from NSSortDescriptor's behavior.So, I set off to make a custom subclass of NSSortDescriptor. I can just override compareObject:toObject:, easy as pie! Right? Er... not quite. There really isn't any information at all in the Cocoa docs about subclassing NSSortDescriptor, and there are a few gotchas that aren't necessarily obvious when implementing the subclass, especially if you're using it with an NSTableView.To begin, I made my subclass (ITSortDescriptor) and overrode the compareObject:toObject: method. This worked fine to begin with, but I discovered that if I clicked on a table column to change the sort order of the table, it would soon revert to the default sort behavior. After some debugging, I discovered that the reason for this was that NSTableView was making copies of the columns' sort descriptors using the NSCopying protocol. NSSortDescriptor implements NSCopying, but apparently its implementation will always return an instance of NSSortDescriptor, even the object being copied is an NSSortDescriptor subclass. So, I implemented copyWithZone: in ITSortDescriptor to return an instance of the subclass rather than a plain NSSortDescriptor. The implementation is pretty straightforward:- (id)copyWithZone:(NSZone*)zone{return [[ITSortDescriptor alloc] initWithKey:[self key] ascending:[self ascending] selector:[self selector]];}This solved the problem for some cases, but there were still times when the column would revert to its old sorting behavior. I narrowed it down to when the table was already sorted by the column in question, and then you clicked on that column again to reverse the sort order. Apparently, NSSortDescriptor's implementation of reversedSortDescriptor suffers from the same problem as its implementation of copyWithZone:. Overriding that in a similar manner then fixed that problem:- (id)reversedSortDescriptor{return [[[ITSortDescriptor alloc] initWithKey:[self key] ascending:![self ascending] selector:[self selector]] autorelease];}To be really cool, instead of having [ITSortDescriptor alloc], that could instead be [[self class] alloc], but that would only really be useful if that's what NSSortDescriptor did itself.So with those modifications, I now have my table sorting just the way I want it. I thought I'd blog this so that anyone else who came across this stuff and was banging their heads against the wall might find it and save themselves some pain.