manicwave

Surf the wave

Unraveling the Mysteries of NSSplitView - Part 1

Permalink

There are many examples of Mac applications that leverage split views to great effect. Apple uses split views in many applications that we use every day - Mail.app, Preview.app, Xcode.app etc. Many third parties use NSSplitView as well. Gauging from the number of posts on the net, there is also a lot of confusion about how to accomplish the basics (or the expected) using the default NSSplitView class provided in the SDK. This point is accentuated by the presence of several third party frameworks that provide these expected behaviors.

Pre-leopard behavior of NSSplitView gave rise to several of the third party frameworks. In 10.5 Apple cleaned up the implementation of NSSplitView and ostensibly provides everything one needs to use split views effectively.

My experiences (and occasional frustrations) with third party frameworks led me to dig in and try to understand what Apple offers, how it works and how (if) I could package that up in a reusable form for use in my own applications.

Requirements

Everyone has there own pet list of requirements for a usable split view solution, but my list includes the following:

  • collapsible subviews - A common UI capability today is a button or key combination that hides a subview
  • Intelligent subview resizing - There are a lot of documented issues with autoresizing Cocoa views - we need a splitview solution that doesn't trash subviews when they are collapsed
  • Ability to specify additional drag areas for splitters
  • Ability to specify custom divider visuals
  • Sexy animation when toggling the visibility of subviews First, it's worth looking at a few of the frameworks that are out there, what problems they solve and those that they don't (from my perspective).

RBSplitView

The excellent RBSplitView emerged in 2004 in the pre-leopard days. In addition to providing the runtime framework, RBSplitView includes an Interface Builder palette that supports IB modification of most of the desired behaviors, including divider selection, collapsible views, and min and max settings for view sizes.

RBSplitView also includes support for animating view adjustments such as collapse/uncollapse.

There are a long list of applications that use RBSplitView .

RBSplitView is, from my experiments, very solid code. Capabilities such as toggling splits works, preserving subview layouts.

The primary issue with RBSplitView is that the animation used is pre Core Animation. Running on my 8 core mac pro, this is not an issue, but it gets a bit jumpy on my MacBook Pro. Further, by implementing the animations in the framework, RBSplitView limits ones ability to leverage a consistent CA based animation scheme throughout the application.

Bottom line - in a 10.6 world, is RBSplitView still the best solution?

BWSplitView

I have to say that I really want to like BWSplitView - Brandon Walkin's framework that provides a modern NSSplitView based solution. On the surface, it meets all of the requirements outlined above. It is supported by a rich IB palette with a slew of conveniences. Brandon's site includes some great screencasts illustrating the promise of BWSplitView.

That said, I struggled in real world usage of BWSplitView. Documented open issues around the splitter drag behavior created some showstopper issues for me.

In fact, it was my attempts to fix some of the BWSplitView issues that led me to explore how NSSplitView works.

Digging in

The canonical example of splitter view behavior is perhaps Mail.app. Two splitters, a vertical split for the Mailboxes and main view. The main view is horizontally split into the messages list and the message viewer.

To understand what NSSplitView offers out of the box, let's build a sample app with a single splitter. If we can understand what is going on with a single split view, we can generalize that as we add additional split views.

In order to verify the subview resizing challenges, one of our subviews will have several standard cocoa controls with autoresizing turned on.

The app looks like this:

NSSplitView-Part1-initial.png

Dragging the divider all the way to the right and then back towards the middle quickly borks up the autoresizing behavior.

NSSplitView-Part1-borked.png

To end part 1 of this exploration, let's add minimum sizes for our split view.

We instantiate a new controller class, we'll call it MySplitViewController (as it stands now, its just a delegate but we'll be adding other functionality to it as we move along).

Instantiate an instance of this class in the XIB and hook the delegate outlet of the NSSplitView outlet to the new class.

There are a bunch of optional methods declared in the NSSplitViewDelegate - we'll implement only two. constrainMinCoordinate and constrainMaxCoordinate. The documentation is sparse on the functionality of these methods, but the header file for NSSplitView does a pretty good job of laying out the basics.

constrainMin... and constrainMax are called repeatedly while the divider is being dragged. In the example implementation, we'll answer proposedMinimum+200 for the constrainMinCoordinate call, which in our example will have the effect of limiting the left subview to 200 pixels. Judicious use of NSLog will show that the proposedMinimum will be 0 -- we're adding 200 and hence the minimum position of the divider will be 200 pixels. When you begin having splitviews with more than two subviews, the concept is the same, but you are now responsible for determining which subview is being referenced. In our example there is only one divider and its index will always be zero.

The constrainMaxCoordinate method works in a similar fashion. The splitview sends in a proposedMaximumPosition that is the width of splitview - in this case, the entire width of the window. We'll answer with that value - 100 resulting in a maximum divider position of the window width - 100 pixels.

@implementation MySplitViewController  
/*
 * Controls the minimum size of the left subview (or top subview in a horizonal NSSplitView)
 */
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex;
{
    NSLog(@"%@:%s proposedMinimum: %f",[self class], _cmd, proposedMinimumPosition);
    return proposedMinimumPosition + 200;
}  
/*
 * Controls the minimum size of the right subview (or lower subview in a horizonal NSSplitView)
 */
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex;
{
    NSLog(@"%@:%s proposedMaximum: %f",[self class], _cmd, proposedMaximumPosition);
    return proposedMaximumPosition - 100;
}

left-size-constrained.png

right-size-constrained.png

After compiling and running, we can now see that both the left and right subview are indeed constrained. Although the 100 pixel extent of the right view does not prevent the view from looking terrible, it does prevent the autoresize funkiness of the first example.

Conclusion and Next Steps

We've only scratched the surface of NSSplitView. We limited the complexity of our NSSplitView example to the simplest possible case and illustrated some of the challenges in using NSSplitView. In the end, we were able to introduce a few delegate methods that exert additional control over the behavior of NSSplitView.

In the next article, we'll add support for collapsible subviews and then add programatic collapsing. Additional topics for future posts will cover adding animated adjustments, controlling the effective drag area, customizing dividers and generalizing the solution.

The code for this segment can be downloaded here: Part 1 Sample Projects