I’ve found, when I try to use Cocoa bindings for my user interface, that it’s never quite flexible enough for what I need.
Instead of trying to list the hurdles I’ve encountered in the abstract, I’m going to walk you through the first steps of a simplified example, called Categorizer, which is also available for download. This way, you can see the things I’ve figured out how to do, in addition to the dealbreakers.
The Categorizer’s data model consists of Things and Categories.
The UI pictured above is the master table, of Categories. The detail table, of Things in the selected Categories, will be to the left. We won’t get to the detail table today.
The first category, "All," is special: you can’t delete it or rename it. It will contain all the things, and is always listed at the top of the category table, which is otherwise alphabetically sorted. You "categorize" things by dragging them onto the rows you’ve made yourself in the category table.
The first issue is sorting. As I said above, categories need to be sorted alphabetically except for the "All," which is always on top.
NSArrayController will automatically sort its array – the contents of the table – according to the Sort Key and Sort Selector of the columns in the table, which are set in Interface Builder. The Sort Key needs to be set to something, it can’t be left blank if I want sorting. So I can’t, for example, try to sort on the entire Category class; I need to pick an attribute. Normally I’d pick something like "name," but in my case I want to into account the special "All" status and name. So I make a "specialSortKey" Category method and specify that in IB.
The table doesn’t start out sorted. I fix this not by manipulating the table, but by manipulating the NSArrayController. I get the sortDescriptorPrototype from the column, stick it in an NSArray, and use it as the parameter for a setSortDescriptors: message to the controller. (See the download for the exact code.) This also selects the column header and sets the indicator image (the little up or down arrow).
Turns out, though, that I don’t want this to act like a normal table column. I want the contents to be sorted the same way all the time, so I don’t want the up or down arrow in the column header that the user can toggle between. Because this would put "All" at the bottom, for one. But because of the array/controller integration, it’s basically not possible to have one without the other if I want to use an NSArrayController. This, it turns out, is dealbreaker #1.
I also want specific behavior when adding a new category. The standard NSArrayController behavior, if I connect the "Add" button to the controller’s add: action, is that a new, empty instance is added to the array and to the table. What I want, in addition, is that you automatically start editing that instance.
My first thought is to connect the "Add" button to a method in my own delegate, newCategory:. This method invokes the controller’s add: method and then starts the editing session.
Unfortunately, the new row is not yet made by the time add: returns. I’m not sure why this would be an asynchronous process, but it is. Oh, wait, here we go, from the documentation: "Note: In Mac OS X v10.4 the target-action methods are deferred so that the error mechanism can provide feedback as a sheet."
I then try implementing MyDelegate’s key-value coding method insertObject:inCategoriesAtIndex: so that, after it does its business, it initiates editing of the added row. But for some reason (maybe because it’s a horrible hack) that doesn’t work either.
This, so far, is dealbreaker #2.
After I’m done editing, I want the renamed category to be re-sorted properly. This is pretty easy, but still involves a gotcha. On 10.4, the controlTextDidEndEditing: NSTableView delegate method is never invoked when you double-click a table cell and edit it in a table that uses an NSArrayController. In my tests, it seems to still work in a table that doesn’t use an NSArryController.
Luckily, there’s another Cocoa API in a superclass of NSTableView, NSControl’s controlTextDidEndEditing:. If I define that in my table delegate, I can use it to send the rearrangeObjects message to my controller, and the renamed category will be immediately resorted.
My last editing issue, and last issue for this post, since it’s getting long, is uniqueness. I don’t want you to be able to enter the same category name twice. If you try to end editing with an invalid name, I want the UI to beep and continue the editing session.
I try to do this in the key-value coding way by implementing the "name"-specific validation method validateName:error: in Category. There’s some finagling, since now Category needs to be able to access the full list of categories in MyDelegate. But, with that achieved, Category’s validateName:error: now returns NO if the new name is the same as any other name.
Can I make it beep? Nope. My only choice of error feedback for the user is an error dialog. There are plenty of options in NSError so I can customize what the dialog says, but no option to do something else.
The download, called Categorizer01.zip, contains the work I’ve described so far, including the behavior I couldn’t get working right. It’s an Xcode 2.1 project, sorry, it can’t be opened in Xcode 2.0.
If my schedule permits, soon I will make a second Categorizer, one that doesn’t use Cocoa bindings, that customizes the behavior as I wanted.