With Swift, can Cocoa Bindings be used to sort by a column in NSTableView?

I'm building a Swift app, and can successfully populate the rows of an NSTableView using Cocoa bindings in a Storyboard. When I hook up a NSTableColumn's Sort Key & Selector, the click-on-column-heading becomes active (the sort-direction icons appear as expected), but the rows are not re-sorted. I've thrashed around a bit, replacing my Swift Array with an NSMutableArray, trying variations on Sort Key, but no joy... Anyone have any clues?

Answered by QuinceyMorris in 296215022

(It's a while since I had to sort a column, so forgive me if I misremember at any point.)


>> I'm using bindings to populate the rows, and I wouldn't expect to set a table view delegate under those circumstances


You almost always end up wanting a delegate for something or other, so it's usual to have one. But you don't need to do anything with "tableView(_:sortDescriptorsDidChange:)" in your scenario. The array controller will do the heavy lifting for you.


>> do bindings just replace the dataSource … ?


Yes, they do. If you use bindings, you don't need a data source (and it can be nil). However, for historical reasons, methods that deal with dragging rows are in the data source protocol, not the delegate protocol. If you need to support dragging, you will need to use a data source after all. In this case, you do not implement the data source methods that the documentation says you must implement. You just implement the ones you need to support dragging.


>> I am already using an NSArrayController to provide the data via bindings.


Yes, that's the right thing to do.


>> Where things don't work is the corresponding NSTableColumn: Under the Attribute Inspector, I can set the Sort Key & Selector


That's only one step. This sets up the NSTableColumn property "sortDescriptorPrototype", which is the descriptor you want to use if the column were to be sorted. To do this via bindings, you also need to bind the table view's "Sort Descriptors" binding to the array controller's "sortDescriptors" property.


When you click on the column-heading to sort the column, the column's actual sort descriptor is constructed, replacing the previous entry in the table view's sort descriptor array, which get pushed to the array controller via the binding, which causes the array controller to sort its content, which causes its "arrangedObjects" proxy array to change, which causes a KVO change notification to be issued to the table view, which redraws its content with the column in the new order. See? Nothing to it!


Note that the sort descriptors aren't saved anywhere further than the array controller, so they're lost when the window closes. If you want to save them as state, you can declare a sortDescriptors property in (say) the view controller, bind the array controller "Sort Descriptors" binding to that property, and the sort descriptors will flow all the way through to the view controller. If your view controller (say) observes its own sortDescriptors properties, it can arrange to write them to (say) user defaults whenever they change, so you can restore them the next time the window is displayed. Nothing to it!


To take this full circle: If your view controller is saving and restoring sort descriptors like this, then there's no real point in setting them in the table columns via the Attribute inspector in IB. You may as well set them in the the view controller's property initially (when there's nothing in user defaults to restore), and let them flow through to the table view. Either technique is fine, since they both amount to the same thing, more or less.

Table view don't sort themselves. They just store per-column sort descriptors which (as you saw) affect the presentation of the UI. Instead, changing the sort descriptors sends a delegate method "tableView(_:sortDescriptorsDidChange:)" (https://developer.apple.com/documentation/appkit/nstableviewdatasource/1532935-tableview), and, as the discussion on that page says, it's up to the data source to sort the data model appropriately.

However, no one actually does it that way. Instead, the usual solution is to put a NSArrayController between the table view and its data source (the "content" of the array controller). In that case, the table view sort descriptors are automatically bound to the array controller's sort descriptors, and the array controller keeps an internal array of sorted object proxies ("arrangedObjects") that it feeds back to the table view column. In this scenario, you get the sorting "for free".

(Since this is done with bindings, you can change the sort descriptors on the table view, and the array controller gets notified via KVO. And vice versa. And you can keep the sort descriptors in your data model too, if you want, and bind the array controller sort descriptors to them, so that all three sets of sort descriptors stay in sync automatically. Yay, KVO!)

QuinceyMorris -- what you're describing is exactly what I want to happen. Years ago I did this stuff in ObjectiveC (there used to be a detailed example in the Apple docs, but it disappeared over the years), and I'm just trying to get back to the same level of proficiency in Swift. When I could get KVO to work, I found it marvelous, but Swift, with its type-safe behaviours has thrown me a real curve ball.


I haven't been exploring delegate method "tableView(_:sortDescriptorsDidChange:)" because I'm using bindings to populate the rows, and I wouldn't expect to set a table view delegate under those circumstances. Or do bindings just replace the dataSource and I can fiddle with the delegate protocol methods?


I am already using an NSArrayController to provide the data via bindings.

In detail, the NSTableView instance has the content binding to the NSArrayController's arrangedObjects.

At each row, for the name column, the NSTextField is bound to the objectValue.name of the containing NSTableCellView. This works OK

Where things don't work is the corresponding NSTableColumn: Under the Attribute Inspector, I can set the Sort Key & Selector

Under the Binding Inspector, I can set the details of the Value binding. I presume one or both of these are relevant, but in guessing appropriate entries, I haven't found anything workable.

Any clues?

Accepted Answer

(It's a while since I had to sort a column, so forgive me if I misremember at any point.)


>> I'm using bindings to populate the rows, and I wouldn't expect to set a table view delegate under those circumstances


You almost always end up wanting a delegate for something or other, so it's usual to have one. But you don't need to do anything with "tableView(_:sortDescriptorsDidChange:)" in your scenario. The array controller will do the heavy lifting for you.


>> do bindings just replace the dataSource … ?


Yes, they do. If you use bindings, you don't need a data source (and it can be nil). However, for historical reasons, methods that deal with dragging rows are in the data source protocol, not the delegate protocol. If you need to support dragging, you will need to use a data source after all. In this case, you do not implement the data source methods that the documentation says you must implement. You just implement the ones you need to support dragging.


>> I am already using an NSArrayController to provide the data via bindings.


Yes, that's the right thing to do.


>> Where things don't work is the corresponding NSTableColumn: Under the Attribute Inspector, I can set the Sort Key & Selector


That's only one step. This sets up the NSTableColumn property "sortDescriptorPrototype", which is the descriptor you want to use if the column were to be sorted. To do this via bindings, you also need to bind the table view's "Sort Descriptors" binding to the array controller's "sortDescriptors" property.


When you click on the column-heading to sort the column, the column's actual sort descriptor is constructed, replacing the previous entry in the table view's sort descriptor array, which get pushed to the array controller via the binding, which causes the array controller to sort its content, which causes its "arrangedObjects" proxy array to change, which causes a KVO change notification to be issued to the table view, which redraws its content with the column in the new order. See? Nothing to it!


Note that the sort descriptors aren't saved anywhere further than the array controller, so they're lost when the window closes. If you want to save them as state, you can declare a sortDescriptors property in (say) the view controller, bind the array controller "Sort Descriptors" binding to that property, and the sort descriptors will flow all the way through to the view controller. If your view controller (say) observes its own sortDescriptors properties, it can arrange to write them to (say) user defaults whenever they change, so you can restore them the next time the window is displayed. Nothing to it!


To take this full circle: If your view controller is saving and restoring sort descriptors like this, then there's no real point in setting them in the table columns via the Attribute inspector in IB. You may as well set them in the the view controller's property initially (when there's nothing in user defaults to restore), and let them flow through to the table view. Either technique is fine, since they both amount to the same thing, more or less.

QuinceyMorris,


Thanks -- things work just fine now. I had overlooked the Sort Descriptor bindings, since visually they weren't well-distinguished from elements of the Content binding in Interface Builder. I need to work on reading the IB details more closely and keeping a list of what I don't understand (for future research).


Richard

With Swift, can Cocoa Bindings be used to sort by a column in NSTableView?
 
 
Q