The Mental Blog
The Mental Blog
2009
The last few years have seen a trend in desktop app design to single window interfaces. The iLife apps, including iTunes, all share this approach, and third party developers have followed Apple’s lead. Where windows once contained a single coherent piece of the interface, they now group together multiple panes.
This is fine from a user perspective, but it does pose a bit of a problem for Cocoa developers. With individual windows containing more and more functionality, the window controllers responsible for keeping them in sync with the model layer have the potential to grow into enormous, unstructured masses of code. Categories can help break things down a bit, but ultimately a more modular approach is needed.
Apple recognized this, and introduced the NSViewController class in Leopard. An NSViewController is to an NSView what a NSWindowController used to be to a single window. Generally, you wouldn’t have a separate NSViewController for every single view, but each logical grouping (ie pane) would be controlled by a single view controller.
I’ve been developing Mental Case 2.0 for a few months now, and I can tell you that NSViewController has completely changed the way the app’s source code is ordered. In fact, structurally it is now very similar to the iPhone version of Mental Case, which is perhaps not surprising, because the iPhone is where view controllers first made their appearance.
This is all well and good, but there is one aspect of NSViewController that Apple left hanging: where does it belong in the Cocoa responder chain? It is a subclass of NSResponder, but there is no automatic support for inserting view controllers into the responder chain. It is not clear why Apple have neglected to address this issue, but it may be that it was thought better to leave this detail to developers, because — as we’ll see — there are several ways to incorporate view controllers into the chain.
When the class first appeared, Cathy Shive and Jonathan Dann wrote an excellent series on view controllers and the responder chain. They considered various designs, and eventually opted for one in which the view controllers would all be patched into the responder chain after the window controller.
When I first saw this solution, I did a double take, because it seemed like a strange choice. The responder chain generally begins at the most specialized object it can find, and moves to more general, high-level objects. For example, the first responder of a mouse click is the view in which the click occurs. If that view doesn’t handle the event, it is passed to its superview, and so forth, up to the window and then the window controller. To insert the view controllers after the window controller seemed odd to me. I had expected each view controller to come directly after its corresponding view in the chain.
It turns out Shive and Dann did address this issue in a separate post. Their first objection to having each view controller inserted directly after its corresponding view was that there was no way short of subclassing NSView to achieve this. Matt Gallagher has independently proposed exactly this solution — subclassing NSView — but Shive and Dann make a very good point: what if you want to have your view controller control an existing view class such as an NSTableView? Using the subclassing approach, you would end up having to subclass NSTableView itself, or insert your table view in a custom class. Not ideal.
The second objection made by Shive and Dann to inserting each view controller directly after its view was that if you take the case of menu validation, in order for a view controller to validate a menu item, its view would have to be in the responder chain. In many cases, a view controller may not be in the responder chain and thus not able to validate and handle action messages sent down the chain by menu items.
This is true for some menu items, but not all. Take a Delete menu item. In this case, it really does matter which view is first responder. If you select an item in the iTunes source list on the left, and select Delete, you expect that item to be deleted, and not an item in the table view on the right. It does make sense to have the view controller follow the view in the chain. Equally, it wouldn’t make sense for a view controller corresponding to the table view on the right to be in the responder chain at all. In the design of Shive and Dann, all view controllers are in every responder chain.
So what’s the solution? I’ve contemplated quite a bit, and here’s what I have decided to do: each NSView has a reference to its NSViewController, in the same way that an NSWindow has a reference to its NSWindowController. The NSViewController is inserted directly after the NSView in the responder chain.
Hang on. What about Shive and Dann’s first objection to this approach, namely that it requires subclassing? It turns out that recent changes in the Objective-C runtime mean that you can now add a reference to every NSView without subclassing, simply using a category. Categories do not allow you to add new instance variables, but you can achieve almost the same effect using associated references.
I was able to piece together a relatively simple implementation using information from the various blog posts above, and this post from the Cocoa with Love blog. You can download the source code here. (Warning: This code requires garbage collection.)
To use this code, drag it into your project, and then simply set the viewController property of any view. I usually do this in the loadView method of the NSViewController itself, though you could also override setView: to do the dirty work. Whichever you choose, it’s dead simple.
That isn’t the end of the story, of course. This design will allow you to handle mouse and key events associated with a particular view, as well as actions from menu items that apply to a particular view. But it doesn’t handle more general actions.
My opinion is that these more general actions should be dealt with at a higher level, in the window controller. I store outlets to all of a window controller’s view controllers, so the window controller can just catch an event, and delegate it to a view controller if appropriate. This is perhaps not ideal, but it is simple and direct, and you only need to do it for more general actions.
NSViewController and the Chain of Responsibility
14/11/09
NSViewController is a relatively new Cocoa class that promises to make enormous monolithic window controllers a thing of the past. But where does it fit into Cocoa’s responder chain?
Photo by Peter Kurdulija
http://flic.kr/p/5Chmwh