There are a bunch of answers for this. In 2019, the best way to do this is to establish constraints on your SplitView panes, then animate the constraints.
Suppose I have a SplitView with three panes: leftPane, middlePane, rightPane. I want to not just collapse the two panes on the side, I want to also want to dynamically resize the widths of various panes when certain views come in or go out.
In IB, I set up a WIDTH constraint for each of the three panes. leftPane and rightPane have widths set to 250 with a priority of 1000 (required).
In code, it looks like this:
@class MyController: NSViewController
{
@IBOutlet var splitView: NSSplitView!
@IBOutlet var leftPane: NSView!
@IBOutlet var middlePane: NSView!
@IBOutlet var rightPane: NSView!
@IBOutlet var leftWidthConstraint: NSLayoutConstraint!
@IBOutlet var middleWidthConstraint: NSLayoutConstraint!
@IBOutlet var rightWidthConstraint: NSLayoutConstraint!
override func awakeFromNib() {
// We use these in our animation, but want them off normally so the panes
// can be resized as normal via user drags, window changes, etc.
leftWidthConstraint.isActive = false
middleWidthConstraint.isActive = false
rightWidthConstraint.isActive = false
}
func collapseRightPane()
{
NSAnimationContext.runAnimationGroup({ (context) in
context.allowsImplicitAnimation = true
context.duration = 0.15
rightWidthConstraint.constant = 0
rightWidthConstraint.isActive = true
// Critical! Call this in the animation block or you don't get animated changes:
splitView.layoutSubtreeIfNeeded()
}) { [unowned self] in
// We need to tell the splitView to re-layout itself before we can
// remove the constraint, or it jumps back to how it was before animating.
// This process tells the layout engine to recalculate and update
// the frames of everything based on current constraints:
self.splitView.needsLayout = true
self.splitView.needsUpdateConstraints = true
self.splitView.needsDisplay = true
self.splitView.layoutSubtreeIfNeeded()
self.splitView.displayIfNeeded()
// Now, disable the width constraint so we can resize the splitView
// via mouse, etc:
self.middleWidthConstraint.isActive = false
}
}
}
extension MyController: NSSplitViewDelegate
{
final func splitView(_ splitView: NSSplitView, canCollapseSubview subview: NSView) -> Bool
{
// Allow collapsing. You might set an iVar that you can control
// if you don't want the user to be able to drag-collapse. Set the
// ivar to false usually, but set it to TRUE in the animation block
// block, before changing the constraints, then back to false in
// in the animation completion handler.
return true
}
final func splitView(_ splitView: NSSplitView, shouldHideDividerAt dividerIndex: Int) -> Bool {
// Definitely do this. Nobody wants a crappy divider hanging out
// on the side of a collapsed pane.
return true
}
}
You can get more complex in this animation block. For example, you could decide that you want to collapse the right pane, but also enlarge the middle one to 500px at the same time.
The advantage to this approach over the others listed here is that it will automatically handle cases where the window's frame is not currently large enough to accommodate "expanding" a collapsed pane. Plus, you can use this to change the panes' sizes in ANY way, not just expanding and collapsing them. You can also have all those changes happen at once, in a smooth, combined animation.
Notes:
- Obviously the views that make up
leftPane, middlePane, and rightPane never change. Those are "containers" to which you add/remove other views as needed. If you remove the pane views from the SplitView, you'll destroy the constraints you set up in IB.
- When using AutoLayout, if you find yourself setting frames manually, you're fighting the system. You set constraints; the autolayout engine sets frames.
- The
-setPosition:ofDividerAtIndex: approach does not work well when the splitView isn't big enough to set the divider where you want it to be. For example, if you want to UN-collapse a right-hand pane and give it 500 width, but your entire window is currently just 300 wide. This also gets messy if you need to resize multiple panes at once.
- You can build on this approach to do more. For example, maybe you want to set minimum and maximum widths for various panes in the splitView. Do that with constraints, then change the constants of the
min and max width constraint as needed (perhaps when different views come into each pane, etc).
CRITICAL NOTE:
This approach will fail if any subview in one of the panes has a width or minimumWidth constraint that has a priority of 1000. You'll get a "can't satisfy constraints" notice in the log. You'll need to make sure your subviews (and their child views, all the way down the hierarchy) don't have a width constraint set at 1000 priority. Use 999 or less for such constraints so that the splitView can always override them to collapse the view.