How to Use NSSplitView in a macOS App
Splitting your views for more functionality
Many macOS apps — like Safari, Finder, Mail, and Xcode — use split views to divide their content into separate areas. This allows a user to decide how to size different parts of the app themselves.
Coming from an iOS background, I was surprised how many fewer references, articles, and documentation exist for macOS development; thus, I decided to write down my experiments on how to use an
In this tutorial, we’ll first add a split view via a storyboard and see how to add more panels. Next, we’ll explore how to recreate Xcode’s segmented control to show and hide these areas programmatically. To improve this transition between collapsed and expanded panels, we’ll add an animation. Finally, we’ll see how to detect when a user manually resizes a panel and how to reflect this update in the segmented control.
Adding an NSSplitView
Let’s start by adding an
NSSplitView to our storyboard. As you can see in the left image below, when starting a new project, you’ll see a window controller and a view controller in your storyboard. To add the split view, simply search for it in the library (accessible via the + button in the upper right corner of Xcode or by pressing Shift+Cmd+L) and drag it onto the existing view controller.
After adding some constraints to position it inside the previous view controller, you could start creating each panel of the split view by adding subviews directly to it, but it’s a better approach to add container views for each panel.
This way each part of the split view has its own separate view controller, which will reduce the responsibilities and the amount of code your initial view controller has.
Again search the library for container views and add them as subviews to the split view. You can see the final setup on the right side of the image below. I also added labels with the texts “Left Panel” and “Right Panel” to the panels, so they have some content to show.
This is what the app looks like right now:
Adding More Panels
By default, a
NSSplitView will have two panels. If you look closely through the configuration options in the inspector, you won’t find an option to add another one like you would for an
NSSegmentControl, for example. So what do you do if you want to have more than two panels?
Looking at the view hierarchy, we can see our split view holds two custom views. We can simply get another one from the library (+ button or Shift+Cmd+L) and drag it inside of our split view. This will add a new area, which we can set up just like the two previous ones above.
Programmatically Show or Hide Panels
Now that we have three panels to hold the content of our app, let’s look at how to programmatically show or hide these areas.
Of cause, a user can drag the dividers to resize them, but many apps have dedicated buttons to do so as well. Safari, for example, has a button in the upper-left corner to show or hide the sidebar, and Xcode offers a segmented control with three segments to change the visibility of the navigator, debug area, and inspector. Additionally, each segment gets updated if the user closes or opens a panel by dragging the divider.
Let’s try to recreate Xcode’s panel control segment. To do so, we need to add a segmented control to the view controller. I also recreated the icons Xcode is using to make it look nicer. We want the first segment to control the left panel and the right segment to control the right one so the middle part is always visible. To allow multi selection on the segment, we need to set its mode to Select Any, and since on start all panels should be visible, we also need to set both segments to be selected. Both settings can be done by selecting segmented control and opening its attribute inspector.
Here’s how the storyboard and the app look like now:
Let’s look at how to handle events on the segmented control:
// 1 — First, we need to add an
IBAction to handle when a segment is clicked by the user. This action provides us the sender as an argument.
// 2 — We can switch over the segment’s
selectedSegment property to know which segment was clicked. Depending on this information, we’ll change the visibility of the left or the right panel. We also need to know whether the segment was selected or unselected. In the first case, we want to show the corresponding panel — otherwise, we want to hide it.
// 3 — If the first segment was clicked, we want to change the visibility of the left panel. If it should be shown, we set 100 points as the new width and call the split view’s method
setPosition(_:ofDividerAt:). This will move the divider at the index 0 to a distance of 100 points from the view’s left border. Otherwise, if the left panel should be collapsed, we’ll set the new position to 0 and thus hide the panel. Finally, we need to call
splitView.layoutSubtreeIfNeeded() to update the layout of the split view.
// 4 — Just like we did with the left panel, we need to set a new position for the divider at index 1. But since it’s the right panel on the right side of the view, this time we’ll set the new position to
view.frame.width — 100 if the panel should be expanded and to
view.frame.width if it should be collapsed.
Now you can run the app, click on the segmented control, and you should be able to see the panels collapse and expand — cool!
Animating the Expanding and Collapsing
We can use an animation to make the transition smoother:
// 1 — To do so, we need to modify the body of
changeRightPanelVisibility(visible:). Instead of updating the dividers position, we call a small helper method which does the same inside an animation.
// 2 — This new method will get the new position and the index of the divider to update.
// 3 — We can use animations by calling
runAnimationGroup and use the given animation context to configure the animation — in this example, the animation will take 0.75 seconds. Finally, inside the closure, we also need to update the split view.
This GIF shows how it should look like in action:
Detecting a Manual Change of the Panel Size
When using Xcode, we can see that the segments are updated when a user drags a divider and closes the navigator, the inspector, or the debug area. Next, we’ll see how we can recreate this behavior.
// 1 — The view controller holding the split view needs to implement the protocol
NSSplitViewDeletate to get notified whenever the position of a divider is updated.
// 2 — We also need outlets to the segmented control and the split view.
// 3 — In
viewDidLoad we can tell the split view this view controller will be its delegate.
// 4 — One of the methods in
splitViewDidResizeSubviews(_:). It gets called whenever a divider is moved.
// 5 — This method receives a
Notification as its parameter. This notification has a property,
userInfo, which is a dictionary containing the index of the moved divider for the key
// 6 — We need two bits of information to update the segmented control: the index of the segment to update and whether it should be selected or unselected. We can get the first piece of information by checking the index of the dragged divider. To get the selection state, we need to check if the panel now has a width of 0 points. If this is the case, the corresponding segment should be unselected — otherwise, it needs to be selected.
// 7 — Lastly, we can set the correct state for the segmented control.
NSSplitView is a powerful view to use in you app. In this tutorial, you saw how to use it in your app to show multiple areas next to each other and how to programmatically expand or collapse the panels just like Xcode does.
If you want more information about view controllers in macOS, you can read this article. It’s not specifically about
NSSplitView, but it explains how to use container views, what the lifecycle of an
NSViewController looks like, and how to use
Thanks for reading!