manicwave

Surf the wave

Unraveling the Mysteries of NSSplitView - Part 2

Permalink

In part 1 of the series, we covered the very basics of NSSplitView. We ended with a NSSplitView with two subviews. We were able to constrain the minimum size of each.

In this part of the series, we are going to add collapsible subviews.

Collapsible subviews

There are several ways to support collapsible subviews. The first is supported directly by NSSplitView. As the user drags a divider to its minimum position, if subviews are collapsible, additional movement will result in the subview snapping shut.

We can support this behavior by adding the canCollapseSubview method.

- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview;
{
    NSView* rightView = [[splitView subviews] objectAtIndex:1];
    NSLog(@"%@:%s returning %@",[self class], _cmd, ([subview isEqual:rightView])?@"YES":@"NO");
    return ([subview isEqual:rightView]);
}

Dragging the divider to the right most edge of the window will result in the right subview snapping shut. The divider will still be show on the rightmost margin of the view. This collapsing behavior is our first bit of good news. When we collapse and uncollapse our right subviews, the layout is not munged as it was in part one when we effectively reduced the right subview width to zero. We'll use the observation later when we add programmatic collapsing.

NSSplitView-Part1a.png NSSplitView-Part1a.png We can hide the divider when the right subview is collapsed.

- (BOOL)splitView:(NSSplitView *)splitView shouldHideDividerAtIndex:(NSInteger)dividerIndex;
{
    NSLog(@"%@:%s returning YES",[self class], _cmd);
    return YES;
}

NSSplitView-Part1a.png

If you keep you're finger on the mouse while moving the divider, you can observe it collapse the right subview and then moving the divider back to the left - uncollapse it.

If you didn't keep your finger on the mouse, the right view collapsed and there is no way now to get the view back. Hovering over the right edge of the window won't do it. There aren't any buttons to press, nothing to double click.

Before we fix this issue, change shouldHideDividerAtIndex back to returning NO.

We'll add one more bit of goodness before we tackle programming collapsing.

Double Click to collapse

NSSplitViewDelegate includes an optional method shouldCollapseSubview:forDoubleClickOnDividerAtIndex: - it does its name suggests. If we answer YES, double-clicking on the divider will collapse the right subview. Double-clicking on the divider while the right subview is collapsed will uncollapse the right subview.

Here's the implementation:

- (BOOL)splitView:(NSSplitView *)splitView shouldCollapseSubview:(NSView *)subview forDoubleClickOnDividerAtIndex:(NSInteger)dividerIndex;
{
    NSView* rightView = [[splitView subviews] objectAtIndex:1];
    NSLog(@"%@:%s returning %@",[self class], _cmd, ([subview isEqual:rightView])?@"YES":@"NO");
    return ([subview isEqual:rightView]);
}

Sweet!

Programmatic Double Click

Programmatic double-click will allow us to hook the action selector of a menu item, a toolbar button or other UI control to a method that will toggle the collapsed state of the collapsible subview.

First we'll add a toolbar to our window to have a convenient place to put the toggle button. Interface Builder.png

In MySplitViewController, we'll add the logic to toggle right subview.

-(IBAction)toggleRightView:(id)sender;
{
    BOOL rightViewCollapsed = [[self mySplitView] isSubviewCollapsed:[[[self mySplitView] subviews] objectAtIndex: 1]];
    NSLog(@"%@:%s toggleInspector isCollapsed: %@",[self class], _cmd, rightViewCollapsed?@"YES":@"NO");
    if (rightViewCollapsed) {
        [self uncollapseRightView];
    } else {
        [self collapseRightView];
    }
}  
-(void)collapseRightView
{  
    NSView *right = [[[self mySplitView] subviews] objectAtIndex:1];
    NSView *left  = [[[self mySplitView] subviews] objectAtIndex:0];
    NSRect leftFrame = [left frame];
    NSRect overallFrame = [[self mySplitView] frame]; //???
    [right setHidden:YES];
    [left setFrameSize:NSMakeSize(overallFrame.size.width,leftFrame.size.height)];
    [[self mySplitView] display];
}

toggleRightView: is the public method that we'll connect UI elements to. It queries the NSSplitView to see if the right subview is collapsed. If not, it calls collapseRightView otherwise, we'll uncollapseRightView:. The key element here is to ensure that the 'collapsed' status of the right view is set correctly. My first several attempts to make this work involved setting the frame width of the right view to 0. NSSplitView did not answer YES to isSubviewCollapsed: under those circumstances. Remember the observation earlier that when we snapped the right view closed and reopened it the view had not been mangled by autoresize logic? That suggests that the view was never shrunk to zero width, but rather hidden. I confirmed this with a F-Script session.

uncollapseRightView: is equally straight forward --

-(void)uncollapseRightView
{
    NSView *left  = [[[self mySplitView] subviews] objectAtIndex:0];
    NSView *right = [[[self mySplitView] subviews] objectAtIndex:1];
    [right setHidden:NO];  
    CGFloat dividerThickness = [[self mySplitView] dividerThickness];  
    // get the different frames
    NSRect leftFrame = [left frame];
    NSRect rightFrame = [right frame];
    // Adjust left frame size
    leftFrame.size.width = (leftFrame.size.width-rightFrame.size.width-dividerThickness);
    rightFrame.origin.x = leftFrame.size.width + dividerThickness;
    [left setFrameSize:leftFrame.size];
    [right setFrame:rightFrame];
    [[self mySplitView] display];
}

The cool thing about all of this is that we're not saving frame rects or adding additional data structures to hold this state. I can assure you that my first several attempts to understand NSSplitView had loads of code to deal with view frames, collapse states, notifications to enable/disable view resizing etc. It is true, at least in this case, that if you're fighting the frameworks, there may be a more enlightened path awaiting discovery.

We can now go back and hide the divider when the right subview is collapsed. The app should now look like this.

NSSplitView-Part2.png

Conclusion and Next Steps

We've accomplished quite a bit. I know, it's hard coded to collapse only the right subview and only handles a single split, but the concepts at work are illustrated plainly. The next few topics for future posts will include adding animated adjustments, controlling the effective drag area, customizing dividers and generalizing the solution.

The code for this segment can be downloaded here: Part-2