Nesting Split Views

posted on

NSSplitView. One of the core components of any Mac app. It has changed a lot over the many years I've been building Mac apps. Early on, it was an archaic class that took a lot of time and effort to perfect. So much so, that some of the earliest "must have" bits of open source code for Mac apps were one of the many NSSplitView subclasses that made managing things like minimum and maximum sizes much easier.

Thankfully things have improved since then, with Auto Layout making sizing as simple as setting a few constraints, and things like NSSplitViewController allowing you to avoid having to implement delegate methods yourself.

Unfortunately, this is only true if you're trying to achieve a simple layout. And today, I was not trying to achieve a simple layout, merely a "simple-ish" layout.

A "simple-ish" layout

The layout I wanted to achieve was a similar setup to Xcode. Xcode has a standard 3 panel layout for its main window, with a sidebar on the left, editors in the middle, and inspectors on the right. If you drag the dividers between any of these it will resize those 3 main views.

However, the editors panel can itself also contain a split views. For example, in Interface Builder the Object List and the Canvas are two parts of a split view. The actual hierarchy looks something like the following image

Now, you may think that this is fairly easy to implement, and for the most part you would be right. But the problem arises when you drag the divider of the inner split view as far as it will go to either the left or the right. As the movie below shows, it will start resizing the outer split view, which is most decidedly not how you'd expect it to work.

Unfortunately, I think NSSplitView is doing some sort of weird stuff with constraints behind the scenes that is overriding the holding priorities of the parent split view. Thankfully we have a way around this: just increase the holding priority. Problem solved!

A wild YAK appeared!

Sadly, programming is never that simple. We only want to increase the holding priority while the inner split view is being dragged, and NSSplitView doesn't provide a way to find out whether a drag is happening by default. You may think that the aptly named splitViewWillResizeSubviews(_:) and splitViewDidResizeSubviews(_:) delegate methods would work for the task, but they get sent for every time the mouse moves, not for the whole drag operation.

"Not to worry", a reasonable dev might say. "We can just override mouseDown(_:) and mouseUp(_:)". Except NSSplitView doesn't actually receive the mouseUp(_:) events, which scuppers that plan.

Thankfully, after a bit of playing around I discovered that what NSSplitView actually does is block on its mouseDown(_:) implementation, meaning that if you call to super, any code after that call won't be invoked until the user releases the mouse button. The reasons NSSplitView does this are probably beyond mere mortals, but now we have a solution.

NestableSplitView

Thankfully the solution ended up being relatively few lines of code, but reaching it required about 4 hours of investigation, including temporarily forgoing Auto Layout completely and going back to "the dark days™" of NSSplitView to see if I could get that to work.

These sorts of problems are common place for developers, but given how unhelpful the Internet seemed to be in providing answers, and how others may very well wish to implement similar layouts in future, I thought it would be worth putting the story out there. If you are interested in the code, you can find it on GitHub. Feel free to use it in your own apps.