Friday, August 8, 2014

NSScrollView and Autolayout

So, you've got an NSScrollView, and you want to add some content that is sized via auto layout constraints. Perhaps you found the "Auto Layout by Example" documentation in the Mac developer library, and discovered that though it contains a section titled Using Scroll Views with Auto Layout, the example is actually for UIScrollView, not NSScrollView. Given the significant differences between the two, that simply won't work.

Here's what you need to do. I'll demonstrate doing this in code, but the same principles apply in Interface Builder as well.



1. Create an NSScrollView. Position it however you like.

    _scrollView = [[NSScrollView alloc] initWithFrame:self.bounds];
    _scrollView.translatesAutoresizingMaskIntoConstraints = YES;
    _scrollView.autoresizingMask = NSViewHeightSizable | NSViewWidthSizable;
    _scrollView.hasVerticalScroller = YES;
    _scrollView.identifier = @"ScrollView";

    [self addSubview:_scrollView];


2. Create an NSView subclass with isFlipped == YES that will contain all your scrolling content.  (If it's not flipped, then the content will prefer to pin to the bottom of the scroll view if not tall enough to require scrolling. That's not what you want.)

    @interface FlippedView : NSView
    @end

    @implementation FlippedView

    - (BOOL)isFlipped {
        return YES;
    }

    @end

3. Create the scrolling container view. Do not use autoresizing masks. Its size doesn't matter too much, but its superview will be the scroll view's contentView, so that makes a convenient default size.

    _scrollContentContainer = [[FlippedView alloc]
                                initWithFrame:_scrollView.contentView.bounds];
    _scrollContentContainer.translatesAutoresizingMaskIntoConstraints = NO;
    _scrollContentContainer.identifier = @"Content container";

4. Set the content container to be the scroll view's document view.

    _scrollView.documentView = _scrollContentContainer;

5. Constrain the top, left, and right edges of the container view to its superview (the contentView). Use Auto Layout constraints to do this, not autoresizing masks.

    NSDictionary *views = NSDictionaryOfVariableBindings(_scrollContentContainer);
    NSArray *hConstraints = [NSLayoutConstraint
                             constraintsWithVisualFormat:@"H:|[_scrollContentContainer]|"
                             options:0 metrics:nil views:views];
    
    NSArray *vConstraints = [NSLayoutConstraint
                             constraintsWithVisualFormat:@"V:|[_scrollContentContainer]"
                             options:0 metrics:nil views:views];
    [_scrollView.contentView addConstraints:hConstraints];
    [_scrollView.contentView addConstraints:vConstraints];
    
6. Add your subviews to the content container however you like. Make sure that the constraints from these subviews will create a natural content size in at least the vertical dimension.

    NSDictionary *buttons = NSDictionaryOfVariableBindings(button, button2);
    NSArray *hConstraints2 = [NSLayoutConstraint
                              constraintsWithVisualFormat:@"V:|-[button]-(1300)-[button2]-|"
                              options: (NSLayoutFormatAlignAllLeft |
                                        NSLayoutFormatAlignAllRight)
                              metrics:nil views:buttons];
    
    NSLayoutConstraint *centerX = [NSLayoutConstraint
                                   constraintWithItem: button
                                   attribute: NSLayoutAttributeCenterX
                                   relatedBy: NSLayoutRelationEqual
                                   toItem:    _scrollContentContainer
                                   attribute: NSLayoutAttributeCenterX
                                   multiplier:1 constant:0];
    
    [_scrollContentContainer addConstraints:hConstraints2];
    [_scrollContentContainer addConstraint:centerX];


Of course, you can do this all in Interface Builder as well, and that's usually what I'd do. It's nice to know how to do it both ways, though. The same principles apply in either case:

  1. Position the NSScrollView externally however you like.
  2. Flipped container view as the scroll view's document view.
  3. Constraints from top, right, and left of container view to its superview.
  4. Constraints from the scrollable content to the container view.

And that's it!