Monday, December 10, 2007

Compound views

Playing around with view hierarchies building some Cocoa UI is pretty fun. It's great how dynamic everything is, and of course everything animates so nicely with Core Animation in Leopard (as you swap subviews for instance).

However, one surprise to me came when trying to build an encapsulated view hierarchy as a kind of reusable compound control (subtree of views). I wanted to be able to create a compound control, with a scroll view at the root and several nested views within that. I wanted to be able to encapsulate both the UI configuration itself (as a NIB of course), and be easily able to create instances of that view hierarchy (and associated controllers) within other UI - as a component.

Building the UI into a NIB is straightforward of course, but soon observed that actually deploying a compound UI in this fashion is a little 'clunky'. Assuming you want to write as little code as possible, and have the best encapsulation (for the component UI and its associated controllers), you will want to have a placeholder view of some kind in the main UI, and then have the subviews loaded and connected up under this placeholder. There are a couple of unsatisfying things about this:
1. What is this view? You are not really wanting it for any real reason other than defining a frame (rectangular extent) to load the subviews into, so it's really just a prototype with no drawing or logic of its own.
2. Yet, it probably needs to be in charge of loading the subviews from the NIB, and establishing their owner. If something tightly associated with the placeholder becomes the owner, with particular logic and responsibility for the subviews, then this somewhat breaks the encapsulation, and maybe some of the point of having separated out some of the UI.
3. If you load a top-level view from a separate NIB and add it as a subview of a pure-placeholder view in the main UI, then you have a seeming redundancy - you have a placeholder, that will only have a single top-level subview and that does nothing really useful otherwise.

These observations had me go look if there was a better way. It seemed to me that there ought to be a standard 'placeholder' view that would be able to load some view hierarchy from a separate NIB at runtime (automatically, and without bespoke logic), and then _replace_ itself in the view hierarchy with a single view from the loaded NIB. The problem of loading specific controlling logic for a view hierarchy delivered in a separate NIB seemed to be adequately fulfilled with Leopard's new NSViewController, but there was no placeholder view that you could have Interface Builder design into your user interfaces that would act as a surrogate for the runtime component.

So I felt compelled to make one.

In order to achieve the goal, you really want to be able to design the placeholder into some UI, and provide the necessary properties at runtime to tell the object where to get its NSViewController and the right NIB to load at runtime. Thankfully, in Leopard, Interface Builder 3.0 now supports plug-ins, and this sort of application is not more than a simple step or two away from the Xcode template for creating a new plug-in. Thus, the SurrogateView and its associated IB3 plug-in was born.
The only slight annoyance in the process was realising that IB3 does not currently support garbage collection, so you still have to write 'old school' to support the design-time environment, even if you can use GC at runtime. These grated a little for one whose motivation around Objective-C and Cocoa is largely underpinned by the fact that it is now a modern memory managed platform.

At runtime, when an embedded SurrogateView is unfrozen from its NIB and receives the -awakeFromNib message, it simply attempts to instantiate a new instance of an appropriate NSViewController subclass (provided as one design-time property), and provides this with the name of the NIB file (provided as the other design-time property). Then it obtains the view object from the controller, sets it to the same location and size as itself, and then it deftly replaces itself as a subview of its parent, with the view loaded from the NIB.

It all seems to work rather well, and I expect to use this plug-in in a good many places in my future UI. I've a good mind to share the SurrogateView IB3 plug-in project as freeware. It's not rocket science (by any means), but it might be useful to others in one form or another.

1 comment:

Scott said...

I have been learning mac programming for about a month and have reached a point on the learning curve where your control would be very useful.