NSTableView, NSArrayController and secondary sortings

  • Hello,

    I have an NSTableView that is bound to an NSArrayController.  There
    are 5 columns in the table view, one of which is an NSDate.  What I
    would like to do is always have the secondary sort be the date
    column.  The default behavior seems to be that the secondary sort is
    based on the previously selected column.

    I have setup sort keys on all the columns in IB and tired various
    things with setSortDescriptors: on NSTableView and NSArrayController,
    but I'm not having any luck.  It seems like what I really need it
    sortDescriptorsDidChange: from the NSTableDataSource protocol, but
    since I'm using bindings that's not available.

    Does anyone have suggestions on how to do programmatically set the
    secondary sort descriptor in this situation?

    Thanks,

    Adhamh
  • On Dec 8, 2007, at 11:00 AM, Adhamh Findlay wrote:

    > Hello,
    >
    > I have an NSTableView that is bound to an NSArrayController.  There
    > are 5 columns in the table view, one of which is an NSDate.  What I
    > would like to do is always have the secondary sort be the date
    > column.  The default behavior seems to be that the secondary sort is
    > based on the previously selected column.
    >
    > I have setup sort keys on all the columns in IB and tired various
    > things with setSortDescriptors: on NSTableView and
    > NSArrayController, but I'm not having any luck.  It seems like what
    > I really need it sortDescriptorsDidChange: from the
    > NSTableDataSource protocol, but since I'm using bindings that's not
    > available.
    >
    > Does anyone have suggestions on how to do programmatically set the
    > secondary sort descriptor in this situation?

    You try handling this in a subclass of NSArrayController.  Override -
    arrangedObjects to always apply the sort-by-date to the arrangement
    returned by [super arrangedObjects].  Or override -sortDescriptors to
    add a sort-by-date descriptor at the end of the array of  descriptors
    that [super sortDescriptors] would return.  This might be
    undesirable, though, if you don't want the sort-by-date behavior to
    always apply when using this array controller.

    ---

    Maybe what you want to do could also be achieved by subclassing
    NSSortDescriptor, applying a sort-by-date after the 'primary'
    sorting?  Using an NSSortDescriptor subclass would be bindings-
    compatible.

    <http://www.fatcatsoftware.com/blog/2007/subclassing-nssortdescriptor-for-fu
    n-and-profit
    >

    I think you'd have to override -compareObject:toObject: to apply the
    sort-by-date condition after the primary comparison.

    Typed in Mail, YMMV:

    - (NSComparisonResult) compareObject:(id)object1 toObject:(id)object2
    {
    NSComparisonResult finalResult;

    // Apply 'primary' sort
    finalResult = [super compareObject:object1 toObject:object2];

    // Apply the sort-by-date if needed; i.e. objects that would
    otherwise initially compare as equal are further sorted by date.
    if ( NSOrderedSame == finalResult ) {
      // compare objects by date, ascending or descending order as needed
      finalResult = ...; //
    }

    return finalResult;
    }

    ---
    Bill
  • On Dec 8, 2007, at 8:36 PM, Bill Garrison wrote:

    > Maybe what you want to do could also be achieved by subclassing
    > NSSortDescriptor, applying a sort-by-date after the 'primary'
    > sorting?  Using an NSSortDescriptor subclass would be bindings-
    > compatible.
    >
    > <http://www.fatcatsoftware.com/blog/2007/subclassing-nssortdescriptor-for-fu
    n-and-profit
    > >

    There are some potentially serious performance problems if you
    subclass NSSortDescriptor.  If your sort key is a keypath and you have
    a large array, you can blow out your autorelease pool on 10.4 because
    Foundation's valueForKeyPath: implementation is inefficient (this is
    fixed in 10.5).  Plus, you don't get the benefit of NSArray's sort
    cache so it's abysmally slow.

    I was unable to get [super compareObject:obj1 toObject:obj2] to work
    if the key was a keypath, so had to implement it like this:

    - (NSComparisonResult)compareObject:(id)object1 toObject:(id)object2 {

        NSString *keyPath = [self key];
        id value1, value2;

        value1 = [object1 valueForKeyPath:keyPath];
        value2 = [object2 valueForKeyPath:keyPath];

        // header says keys may be key paths, but super doesn't work
    correctly when I pass in a key path; therefore, we'll just ignore
    super altogether
        typedef NSComparisonResult (*comparatorIMP)(id, SEL, id);

        SEL selector = [self selector];
        comparatorIMP comparator = (comparatorIMP)[value1
    methodForSelector:selector];
        NSComparisonResult result = comparator(value1, selector, value2);

        return [self ascending] ? result : (result *= -1);
    }

    I put together a demo project for this at http://homepage.mac.com/amaxwell/.Public/kvcTest.zip
      but haven't looked at it for a long time.  YMMV.

    --
    adam
  • On Dec 9, 2007, at 12:34 AM, Adam R. Maxwell wrote:
    >
    > There are some potentially serious performance problems if you
    > subclass NSSortDescriptor.  If your sort key is a keypath and you
    > have a large array, you can blow out your autorelease pool on 10.4
    > because Foundation's valueForKeyPath: implementation is inefficient
    > (this is fixed in 10.5).  Plus, you don't get the benefit of
    > NSArray's sort cache so it's abysmally slow.

    [snip]

    > I put together a demo project for this at http://homepage.mac.com/amaxwell/.Public/kvcTest.zip
    > but haven't looked at it for a long time.  YMMV.

    I ran the demo project on an Intel MacBook w/10.5 and copied the
    original sort-by-keypath tests to make another set of tests that
    sorted by a simple key.

    There's definitely a relative performance hit in subclassing
    NSSortDescriptor for both cases.

    2007-12-09 01:08:49.339 kvcTest[1337:10b] Sorting by key:
    2007-12-09 01:08:58.371 kvcTest[1337:10b] BDSKTableSortDescriptor:
    9.014829 seconds
    2007-12-09 01:09:04.006 kvcTest[1337:10b] NSSortDescriptor:
    5.628339 seconds
    2007-12-09 01:09:16.325 kvcTest[1337:10b] FastTrivialSortDescriptor:
    12.314142 seconds
    2007-12-09 01:09:27.408 kvcTest[1337:10b] TrivialSortDescriptor:
    11.079273 seconds

    2007-12-09 01:09:27.891 kvcTest[1337:10b] Sorting by keypath:
    2007-12-09 01:09:45.998 kvcTest[1337:10b] BDSKTableSortDescriptor:
    18.102415 seconds
    2007-12-09 01:09:52.134 kvcTest[1337:10b] NSSortDescriptor:
    6.133027 seconds
    2007-12-09 01:10:29.562 kvcTest[1337:10b]
    FastTrivialSortDescriptor:    37.424652 seconds
    2007-12-09 01:11:05.396 kvcTest[1337:10b] TrivialSortDescriptor:
    35.830598 seconds

    The demo tests sorting an array of 200,000 objects:

    @interface ValueObject : NSObject
    {
        NSString *title;  // String containing a randomized value.
        NSDictionary *dictionary; // contains above title string under key
    'title'
    }
    @end

    ----

    Maybe you'll never have 200,000 objects managed by your array
    controller, so the performance difference may not be an issue.

    On the other hand, maybe NSArrayController would be the better
    location for implementing a secondary sorting.

    - Bill
  • > I have an NSTableView that is bound to an NSArrayController.  There
    > are 5 columns in the table view, one of which is an NSDate.  What I
    > would like to do is always have the secondary sort be the date
    > column.  The default behavior seems to be that the secondary sort is
    > based on the previously selected column.

    I would disable sorting by clicking the column header altogether.
    For each column uncheck the Create sort descriptors binding option and
    remove any key in the column inspector.

    Then in your controller object create 4 arrays of sort descriptors:
    "FirstName-Ascending + Date-Ascending", "LastName-Ascending + Date-
    Ascending" and so on.

    In IB, add a segmented control or something similar and have its
    action set the sort descriptors of your array controller. Voilà. You
    can even have your segmented control selectedTag binding set to a
    value of user defaults to get the table already sorted as it was the
    last time your app was run.

    If you really want the user to be able to use the regular header
    method, I guess it could be possible to use some delegate method to
    handle the single click in the header, swap the sort descriptors then
    highlight by hand the header. But I can’t help you with this. Besides,
    current cocoa header highlighting scheme will never indicate precisely
    which is the secondary key whereas the segmented control titles or
    icons can be very clear to the user.

    As I frequently create sort descriptors arrays, I added the following
    methods in a category of NSSortDescriptor.

    Flofl.

    +(NSArray *)ascendingDescriptorsForKey:(NSString *)key {

    NSSortDescriptor * tempDescriptor = [[[NSSortDescriptor alloc]
                   initWithKey:key
                     ascending:YES] autorelease];

        return [NSArray arrayWithObject: tempDescriptor];
    }

    +(NSArray *)ascendingDescriptorsForKeys:(NSString *)firstKey,... {

        NSMutableArray * tempArray = [NSMutableArray arrayWithCapacity:3];
        NSSortDescriptor * tempDescriptor;
        va_list keysList;
        id key;

        if (firstKey) {
    // First key:
    tempDescriptor = [[[NSSortDescriptor alloc]
                   initWithKey:firstKey
                     ascending:YES] autorelease];

    [tempArray addObject:tempDescriptor];

    // Other keys:
    va_start(keysList, firstKey);
    while (key = va_arg(keysList, id)) {
           tempDescriptor = [[[NSSortDescriptor alloc]
                   initWithKey:key
                     ascending:YES] autorelease];

           [tempArray addObject:tempDescriptor];
    }
    va_end(keysList);
        }

        return [NSArray arrayWithArray:tempArray];
    }
  • On Dec 8, 2007, at 8:00 AM, Adhamh Findlay wrote:

    > I have an NSTableView that is bound to an NSArrayController.  There
    > are 5 columns in the table view, one of which is an NSDate.  What I
    > would like to do is always have the secondary sort be the date column.
    >
    Taking this literally (i.e. it doesn't matter what other sorting there
    is, the date sort should always be second), you can create a subclass
    of NSArrayController that adds a date sort descriptor in the correct
    place, along the lines of the example shown below.

    mmalc

    @interface SortByDate2ArrayController : NSArrayController
    {
        NSMutableArray *sortDescriptors;
    }
    @end

    @implementation SortByDate2ArrayController

    - (NSArray *)sortDescriptors
    {
        return sortDescriptors;
    }

    - (void)setSortDescriptors:(NSArray *)newDescriptors
    {
        if (sortDescriptors == newDescriptors)
        {
            return;
        }

        [sortDescriptors release];

        sortDescriptors = [newDescriptors mutableCopy];
        NSSortDescriptor *dateDescriptor = nil;

        for (NSSortDescriptor *descriptor in newDescriptors)
        {
            if ([[descriptor key] isEqualToString:@"date"])
            {
                dateDescriptor = descriptor;
                [sortDescriptors removeObject:descriptor];
            }
        }

        if (dateDescriptor == nil)
        {
            dateDescriptor = [[[NSSortDescriptor alloc]
    initWithKey:@"date" ascending:YES] autorelease];
        }

        NSInteger dateIndex = 1;
        if ([sortDescriptors count] == 0)
        {
            dateIndex = 0;
        }

        [sortDescriptors insertObject:dateDescriptor atIndex:dateIndex];
        [self rearrangeObjects];
    }

    @end
previous month december 2007 next month
MTWTFSS
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            
Go to today