14

I have an NSSplitView that's having two panes - a sidebar table view on the left and a web view on the right one. I also have a delegate set that's handling constraints for the sidebar like this:

- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)dividerIndex {
    return 500.0f;
}

- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
    return 175.0f;
}

- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview {
    return NO;
}

It means that the sidebar can only be resized between 175 and 500 pixels and this works fine when using the divider handle. But when resizing the whole window the divider gets repositioned out of these constraints.

Does anybody know how to control this?

Additionally: If I want to store the user's choice of sidebar width, is it a good thought to read it out, save it to a preferences file and restore it later, or is there a more straight-forward way to do this? I noticed that the window's state gets saved in some cases - is this generally happening or do I have to control it?

Thanks in advance

Arne

paulmelnikow
  • 16,895
  • 8
  • 63
  • 114
arnekolja
  • 1,687
  • 5
  • 22
  • 29

9 Answers9

6

I initially implemented the NSSplitView delegate functions and ended up with a lot of code to try to do something so simple as limit the minimum size for each of the split view sides.

I then changed my approach and found a clean and extremely simply solution. I simply set a auto layout constant for a width (>= to my desired minimum size) on the NSView for one side of the NSSplitView. I did the same on my other side. With these two simple constraints the NSSplitView worked perfectly without the need for delegate calls.

Sangram Shivankar
  • 3,535
  • 3
  • 26
  • 38
Marty R
  • 61
  • 1
  • 3
  • This is the best approach. For Maximum size, set an auto layout constant for a width (<= to the desired maximum size) on the NSView. Do it for both sides. – KamyFC Sep 01 '20 at 03:09
4

What you are looking for is:

- (void)splitView:(NSSplitView*)sender resizeSubviewsWithOldSize:(NSSize)oldSize

[sender frame] will be the new size of your NSSplitView after the resize. Then just readjust your subviews accordingly.

Mike A
  • 2,501
  • 2
  • 23
  • 30
  • for me, this doesn't get called the first time that setFrame: is called. However, if I subclass NSSplitView and override resizeSubviewsWithOldSize, that does get called. – Keith Knauber Sep 14 '16 at 19:00
3

The problem is that when the NSSplitView itself is resized, -adjustSubviews gets called to do the work, but it plain ignores the min/max constraints from the delegate!

However -setPosition:ofDividerAtIndex: does take the constraints into account.

All you need to do is combine both - this example assumes an NSSplitView with only 2 views:

- (CGFloat)splitView:(NSSplitView*)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
  return 300;
}

- (CGFloat)splitView:(NSSplitView*)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
  return (splitView.vertical ? splitView.bounds.size.width : splitView.bounds.size.height) - 500;
}

- (void)splitView:(NSSplitView*)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
  [splitView adjustSubviews];  // Use default resizing behavior from NSSplitView
  NSView* view = splitView.subviews.firstObject;
  [splitView setPosition:(splitView.vertical ? view.frame.size.width : view.frame.size.height) ofDividerAtIndex:0];  // Force-apply constraints afterwards
}

This appears to work fine on OS X 10.8, 10.9 and 10.10, and is much cleaner than the other approaches as it's minimal code and the constraints are not duplicated.

Pol
  • 3,848
  • 1
  • 38
  • 55
  • This also worked well for me using `- (CGFloat)splitView:(NSSplitView *)splitView constrainSplitPosition:(CGFloat)proposedPosition ofSubviewAt:(NSInteger)dividerIndex` instead of min/max coordinates. – danomatika Apr 23 '19 at 12:28
2

An alternative way to solve this is using splitView:shouldAdjustSizeOfSubview:

I've found this much simpler for my purposes.

For example, if you want to prevent the sidebarTableView from ever being smaller than your 175 minimum width then you can do something like this (assuming you made sidebarTableView an outlet on your view controller/delegate);

- (BOOL)splitView:(NSSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)subview
{
    if ((subview==self.sidebarTableView) && subview.bounds.size.width<=175) {
        return NO;
    }
    return YES;
}
Ira Cooke
  • 1,325
  • 12
  • 20
  • 1
    Ira, your suggestion is spot on for easily preventing the left splitter pane (subview) from getting too small. HOWEVER, when resizing the right edge of the window, once the subview hits the minimum, the left pane will remain at the minimum size, and will NO LONGER grow proportionally with the right side pane. If shouldAdjustSizeOfSubview passed an indicator if it was growing or shrinking, then that method might work flawlessly. – SMGreenfield Sep 25 '21 at 22:06
1

Here's my implementation of -splitView:resizeSubviewsWithOldSize::

-(void)splitView:(NSSplitView *)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
    if (![splitView isSubviewCollapsed:self.rightView] &&
        self.rightView.frame.size.width < 275.0f + DBL_EPSILON) {
        NSSize splitViewFrameSize = splitView.frame.size;
        CGFloat leftViewWidth = splitViewFrameSize.width - 275.0f - splitView.dividerThickness;
        self.leftView.frameSize = NSMakeSize(leftViewWidth,
                                             splitViewFrameSize.height);
        self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
                                          0.0f,
                                          275.0,
                                          splitViewFrameSize.height);
    } else
        [splitView adjustSubviews];
}

In my case, rightView is the second of two subviews, which is collapsible with a minimum width of 275.0. leftView has no minimum or maximum and is not collapsible.

paulmelnikow
  • 16,895
  • 8
  • 63
  • 114
0

I used

- (void)splitView:(NSSplitView*)sender resizeSubviewsWithOldSize:(NSSize)oldSize

but instead of changing the subview frame, I used

    [sender setPosition:360 ofDividerAtIndex:0]; //or whatever your index and position should be

Changing the frame didn't give me consistent results. Setting the position of the divider did.

0

Maybe too late for the party, however this is my implementation of resizeSubviewWithOldSize:. In my project I need a vertical resizable NSplitView with leftView width between 100.0 and 300.0; no 'Collapsing' taken in account. You should take care of all possible dimensions for the subviews.

-(void)splitView:(NSSplitView *)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
    if (self.leftView.frame.size.width >= kMaxLeftWidth) {
        NSSize splitViewFrameSize = splitView.frame.size;
        CGFloat leftViewWidth = kMaxLeftWidth;
        CGFloat rightViewWidth = splitViewFrameSize.width - leftViewWidth - splitView.dividerThickness;

        self.leftView.frameSize = NSMakeSize(leftViewWidth,
                                             splitViewFrameSize.height);

        self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
                                          0.0f,
                                          rightViewWidth,
                                          splitViewFrameSize.height);
    } else if (self.leftView.frame.size.width <= kMinLeftWidth) {
        NSSize splitViewFrameSize = splitView.frame.size;
        CGFloat leftViewWidth = kMinLeftWidth;
        CGFloat rightViewWidth = splitViewFrameSize.width - leftViewWidth - splitView.dividerThickness;

        self.leftView.frameSize = NSMakeSize(leftViewWidth,
                                             splitViewFrameSize.height);

        self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
                                          0.0f,
                                          rightViewWidth,
                                          splitViewFrameSize.height);
    } else {
        NSSize splitViewFrameSize = splitView.frame.size;
        CGFloat leftViewWidth = self.leftView.frame.size.width;
        CGFloat rightViewWidth = splitViewFrameSize.width - leftViewWidth - splitView.dividerThickness;

        self.leftView.frameSize = NSMakeSize(leftViewWidth,
                                             splitViewFrameSize.height);

        self.rightView.frame = NSMakeRect(leftViewWidth + splitView.dividerThickness,
                                          0.0f,
                                          rightViewWidth,
                                          splitViewFrameSize.height);
    }
}
valvoline
  • 7,737
  • 3
  • 47
  • 52
0

I've achieved this behavior by setting the holdingPriority on NSSplitViewItem to Required(1000) for the fixed side in Interface Builder. You can then control the width for the fixed side by setting a constraint on the underlying NSView.

Felix
  • 776
  • 8
  • 16
0

I just needed to do this, and came up with this method which is a bit simpler than previous examples. This code assumes there are IBOutlets for the left and right NSScrollViews of the NSSplitView container, as well as CGFloat constants for the minimum size of the left and right views.

#pragma mark - NSSplitView sizing override

//------------------------------------------------------------------------------
// This is implemented to ensure that when the window is resized that our main table
// remains at it's smallest size or larger (the default proportional sizing done by
// -adjustSubviews would size it smaller w/o -constrainMinCoordiante being called).

- (void) splitView: (NSSplitView *)inSplitView resizeSubviewsWithOldSize: (NSSize)oldSize
{
    // First, let the default proportional adjustments take place
    [inSplitView adjustSubviews];

    // Then ensure that our views are at least their min size
    // *** WARNING: this does not handle allowing the window to be made smaller than the total of the two views!

    // Gather current sizes
    NSSize leftViewSize = self.leftSideView.frame.size;
    NSSize rightViewSize = self.rightSideView.frame.size;
    NSSize splitViewSize = inSplitView.frame.size;
    CGFloat dividerWidth = inSplitView.dividerThickness;

    // Assume we don't have to resize anything
    CGFloat newLeftWidth = 0.0f;

    // Always adjust the left view first if we need to change either view's size
    if( leftViewSize.width < kLeftSplitViewMinSize )
    {
        newLeftWidth = kLeftSplitViewMinSize;
    }
    else if( rightViewSize.width < kRightSplitViewMinSize )
    {
        newLeftWidth = splitViewSize.width - (kRightSplitViewMinSize + dividerWidth);
    }

    // Do we need to adjust the size?
    if( newLeftWidth > 0.0f )
    {
        // Yes, do so by setting the left view and setting the right view to the space left over
        leftViewSize.width = newLeftWidth;
        rightViewSize.width = splitViewSize.width - (newLeftWidth + dividerWidth);

        // We also need to set the origin of the right view correctly
        NSPoint origin = self.rightSideView.frame.origin;
        origin.x = splitViewSize.width - rightViewSize.width;
        [self.rightSideView setFrameOrigin: origin];

        // Set the the ajusted view sizes
        leftViewSize.height = rightViewSize.height = splitViewSize.height;
        [self.leftSideView setFrameSize: leftViewSize];
        [self.rightSideView setFrameSize: rightViewSize];
    }
}
Lane Roathe
  • 450
  • 4
  • 7