Tuesday, February 12, 2008

NSOutlineView mysteries

I gather from various mailing list histories that NSOutlineViews in Leopard are considerably improved over times of yore (Tiger)... and as a side note, it seems Leopard is a really great time to be learning Cocoa - not just because of Objective-C 2.0, but the considerable number of improvements in Cocoa. Many little holes have been filled and conveniences added, to say nothing of the big new features.

For me today it was time to get to grips with tree controls, i.e. NSOutlineView. Having relatively easily hooked up my Core Data model to one NSOutlineView, I proceeded to add yet another (in a tab view) to show the same model in a different way. The way that NSTreeController bindings work is really nice, in that you can have several 'protocols' (sort of 'key schemas') stamped on the model objects that allow descent through the model hierarchically in different ways.

Custom cells went well too, and I like the new SourceView style that you can turn on to get the similar appearance to iTunes, Finder etc. for those 'left pane' hierarchy navigators.

I then came to trying to hook up the autosave feature of NSOutlineView. In this case, autosave remembers a user's outline state (expanded/collapsed nodes) and can restore the users state next time they launch the application. This is one of those great framework features, providing wonderful user experience almost for free. Well, it would be if I could figure out the two methods I have to write to support it!

Once you provide a key to save the state under in the user's application preferences, you have two symmetrical methods to write. One of these is sent a tree 'item' and you are supposed to return an object 'suitable for archiving', and the other does the reverse - i.e. supposedly maps an archive object to a tree item.

Now, the documentation is a little vague, so I consulted Google for some mailing list hits. The one decent write-up I found suggested that NSKeyedArchiver and its reciprocal unarchiver could simply be applied to the item to convert in each direction. That seemed like a reasonable suggestion to me, so I tried it. Unfortunately, it doesn't work - at least not in Leopard.

Leopard has introduced a 'public' proxy object for tree nodes that NSTreeController creates around the model objects it finds and introduces into the NSOutlineView. Now, I understand that there was always some kind of 'opaque' proxy object before Leopard, but if the post is to be believed, then this object was able to archive and unarchive itself (i.e. it must have conformed to the NSCoding protocol). Nowadays, at least, this isn't the case, and the requisite error messages appear on the console.

So, if you cannot directly transform a tree item into something that can be inserted into the User Preferences, what should you be doing? In fact, the biggest question isn't so much what you could create as an archive object, but rather what exactly an unarchived object is supposed to be in relation to the 'new' outline that one will find when depersisting. Am I supposed to search for the exact tree item matching some key I wrote out? Is it sufficient to build a new NSTreeItem and have it match a different instance already in the tree - using associative semantics? The docs don't elucidate, and I have more experimenting to do tomorrow.

While thinking about this problem, it occurred to me that if I had to actually go find the matching NSTreeNode in the current outline content that matched some saved properties, then I'd need to be able to search or filter the existing nodes. However, I could find no API that would do this. Not on NSOutlineView, NSTreeController, NSTableView or NSObjectController (the latter being the two pertinent superclasses). This came as a bit of a shock, suggesting as it does that you are not supposed to ever need to do this, and perhaps reinforcing the notion that one shouldn't have to search the current tree to find matching instances of some persisted items in order to reset their expanded state.

At that time I decided to park the autosave problem and move on to another issue - that of synchonising the selection between my two outlines in the two tab views. The problem here is that, for any selection, the index paths describing the selected items will be different - because the outline schemes are different - even though the selectable items in the two trees are the same. Whatever eventing scheme is used to detect a change in selection (more about this in a moment), the selection from one tree needs to be transformed into the other outlines scheme. Because the relationship between them happens not to be a simple function, this effectively means being able to look up the selected object from one outline in the other tree. Oops, we're back here again.

This time I dove right in and implemented a fairly vanilla tree search routine and packaged it as an NSTreeController category. All the time suspicious that I was doing something unnecessary and for which there *must* be a more elegant solution, it is nonetheless true that Leopard's addition of NSTreeNode makes it easy enough to search through items that are already in the tree. I'm not exactly sure how NSTreeNode works in conjunction with the parsimonious fetching of model objects by NSTreeController, though it possibly faults the requesting of additional items from NSTreeController if you ask for child nodes it hasn't got yet. If this is how it works it could be a bad thing in some kinds of search, but in any case I achieved a working search (based on the represented model object of the tree item).

With this ability to find equivalent nodes in another tree, I was able to register Key Value Observers on the selected objects of each tree and carefully update the other tree (if a different node was selected - so as to avoid infinite loops). Job done.

Looking at the code though, I'm a little dissatisfied (!). Isn't binding supposed to allow you to connect objects together directly in an (almost) code free way. As I understand it, it is only the asymmetrical selection that is preventing this in this case. What if this could be overcome in the binding itself? Well, bindings support a concept called value transformers, and this sounds like just the medicine to convert between two selection index paths in the two trees. Moreover, my understanding is that KVB handles cyclicity automatically, so it is even more appealing to be synchonising two views in this way.

So, tomorrow I will begin with an experiment to create a subclass of NSValueTransformer. This will have an instance that lives in the NIB file itself, with outlets to connect to the two trees. It will register itself as an active transfomer on -awakeFromNib, and be referenced in selection bindings between the two outline views. If that works, it will be one of those sweet Cocoa moments - and I'll just have the autosave (de)persistence left to grok :-)

No comments: