7

I've would like to not sort the parent nodes of my NSOultineView.

The datasource of my outline view is a NSTreeController.

When clicking on a column header, I would like to sort the tree only from the second level of the hierarchy, and their children and leave the parent nodes in the same order.

UPDATE This is how I bind columns to the values and assign the sort descriptor.

    [newColumn bind:@"value" toObject:currentItemsArrayController withKeyPath:[NSString stringWithFormat:@"arrangedObjects.%@", metadata.columnBindingKeyPath] options:bindingOptions];
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:metadata.columnSortKeyPath ascending:YES selector:metadata.columnSortSelector];
    [newColumn setSortDescriptorPrototype:sortDescriptor];
aneuryzm
  • 63,052
  • 100
  • 273
  • 488
  • Did you try to set custom `sortDescriptorPrototype` of the NSTableColumn instance you click at? – Daniyar May 13 '15 at 14:12
  • @Astoria Yes! I've tried that, and it worked for that column (with the data from the parent nodes). But the children nodes have data in other columns, and it should be possible to sort the children by clicking on the such column headers. However, when clicking on such columns also the parent nodes order is affected. – aneuryzm May 13 '15 at 17:13
  • try to set `sortDescriptorPrototype` for both columns. – Daniyar May 14 '15 at 05:57
  • @Astoria No this approach doesn't work. I have 40 columns. The children have fields for all these columns. And they must be sortable (for the children). If I just remove the sorting, than the children are not sortable anymore. – aneuryzm May 15 '15 at 14:14

1 Answers1

2

You can use custom comparator.

To demonstrate the basic idea of this approach, let's assume you use NSTreeNode as your tree node class. All root nodes are stored in an NSArray named content, and your three root nodes are cat, dog and fish:

NSTreeNode *cat = [NSTreeNode treeNodeWithRepresentedObject:@"cat"];
// ...the same for dog and fish
self.content = @[cat, dog, fish];

Then, create your NSSortDescriptor prototype. Note that you should use self as the key instead of the string you are comparing (in this case representedObject) to get access to the raw node object. In the comparator, check if the object is contained in your root objects array. If YES, just return NSOrderedSame as it will keep the order unchanged from the initial order in your content array, otherwise use the compare: method to do a standard comparison.

NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES comparator:^NSComparisonResult(NSTreeNode *obj1, NSTreeNode *obj2) {

    if ([self.content containsObject:obj1] && [self.content containsObject:obj2]) {
        return NSOrderedSame;
    }

    return [obj1.representedObject compare:obj2.representedObject];
}];

[[outlineView.tableColumns firstObject] setSortDescriptorPrototype:sortDescriptor];

Edit 2

If you have multiple columns that need to be sorted separately, you cannot use self as the key for every column, as the key should be unique among all columns' sortDescriptorPrototypes. In this case, you can create a custom object as the representedObject and wrap all your data including a pointer back to the tree node in the object.

Edit 1

The correctness of the above-mentioned approach requires NSOutlineView to sort its rows using a stable sort algorithm. That is, identical items will always retain the same relative order before and after sorting. However, I couldn't find any evidence in Apple documentation regarding the stability of the sorting algorithm used here, although from my experience the approach above will actually work.

If you are feeling unsafe, you can explicitly compare your root tree nodes based on whether current order is ascending or descending. To do that, you need an ivar to save the current order. Just implement the outlineView:sortDescriptorsDidChange: delegate method:

- (BOOL)outlineView:(NSOutlineView *)outlineView sortDescriptorsDidChange:(NSArray *)oldDescriptors {
    ascending = ![oldDescriptors.firstObject ascending];
    return YES;
}

And change return NSOrderedSame to:

if ([self.content containsObject:obj1] && [self.content containsObject:obj2]) {
    NSUInteger index1 = [self.content indexOfObject:obj1];
    NSUInteger index2 = [self.content indexOfObject:obj2];
    if (ascending)
        return (index1 < index2) ? NSOrderedAscending : NSOrderedDescending;
    else
        return (index1 < index2) ? NSOrderedDescending : NSOrderedAscending;
}

Edit 3

If you cannot implement outlineView:sortDescriptorsDidChange: for whatever reason, you can manually attach an observer to your outlineView's sortDescriptors array:

[outlineView addObserver:self forKeyPath:@"sortDescriptors" options:NSKeyValueObservingOptionNew context:nil];

In this way you can get notified when user clicked the header as well. After that don't forget to implement the following observing method, as a part of KVO process:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"sortDescriptors"]) {
        // Again, as a simple demonstration, the following line of code only deals with the first sort descriptor. You should modify it to suit your need.
        ascending = [[change[NSKeyValueChangeNewKey] firstObject] ascending];
    }
}

To prevent memory leaking, you have to remove this observer before outlineView is deallocated(if not earlier). If you are not familiar with KVO, please make sure to check out Apple's guide.

Renfei Song
  • 2,941
  • 2
  • 25
  • 30
  • hey thanks very good answer. I've just read your edit: unfortunately I can't use outlineView:sortDescriptorsDidChange: because I've not used the data source methods (I'm instead programmatically binding the values). So if I add a datasource, I've to override all require methods. (Instead I'm still studying the comparator approach, come back soon). – aneuryzm May 18 '15 at 11:54
  • 1
    @Patrick If you cannot implement `outlineView:sortDescriptorsDidChange:`, you can manually attach an observer to your `outlineView`'s `sortDescriptors` array: `[outlineView addObserver:self forKeyPath:@"sortDescriptors" options:NSKeyValueObservingOptionNew context:nil];` In this way you can get notified when user clicked the header as well. – Renfei Song May 18 '15 at 12:06
  • Yes, cool. I'm now using the observer: however how could I introduce the comparison function at this point? Please see the Update in my question, I've added the code. I've many custom columns, and I'm binding and setting a different sort descriptor for each of them. – aneuryzm May 18 '15 at 13:00
  • You have to make sure that the key of the sort descriptors is different for all columns, and you have access to your root node object through the key path you use. See my update. – Renfei Song May 18 '15 at 13:46
  • In my answer I am using a comparator which is a block. It would be fine if you prefer a custom sort selector since it works almost the same as a block, except that you have to implement your sort selector in your compared object's class. – Renfei Song May 18 '15 at 13:48
  • But how do I know the current "ascending" value, if I don't use outlineView:sortDescriptorsDidChange, since I can't implement data source methods ? – aneuryzm May 18 '15 at 14:36