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.