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:
Dragging the divider all the way to the right and then back towards the middle
quickly borks up the autoresizing behavior.
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;
}
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