43

The following code (called in viewDidLoad) results in a fully red screen. I would expect it to be a fully green screen. Why is it red? And how can I make it all green?

UIScrollView* scrollView = [UIScrollView new];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];

UIView* contentView = [UIView new];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
contentView.backgroundColor = [UIColor greenColor];
[scrollView addSubview:contentView];

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView,contentView);

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:0 views:viewDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics:0 views:viewDict]];

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:0 views:viewDict]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|" options:0 metrics:0 views:viewDict]];
dragosaur
  • 818
  • 1
  • 8
  • 10

3 Answers3

73

Constraints with scroll views work slightly differently than it does with other views. The constraints between of contentView and its superview (the scrollView) are to the scrollView's contentSize, not to its frame. This might seem confusing, but it is actually quite useful, meaning that you never have to adjust the contentSize, but rather the contentSize will automatically adjust to fit your content. This behavior is described in Technical Note TN2154.

If you want to define the contentView size to the screen or something like that, you'd have to add a constraint between the contentView and the main view, for example. That's, admittedly, antithetical to putting content into the scrollview, so I probably wouldn't advise that, but it can be done.


To illustrate this concept, that the size of contentView will be driven by its content, not by the bounds of the scrollView, add a label to your contentView:

UIScrollView* scrollView = [UIScrollView new];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];

UIView* contentView = [UIView new];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
contentView.backgroundColor = [UIColor greenColor];
[scrollView addSubview:contentView];

UILabel *randomLabel = [[UILabel alloc] init];
randomLabel.text = @"this is a test";
randomLabel.translatesAutoresizingMaskIntoConstraints = NO;
randomLabel.backgroundColor = [UIColor clearColor];
[contentView addSubview:randomLabel];

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView, contentView, randomLabel);

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:0 views:viewDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics:0 views:viewDict]];

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:0 views:viewDict]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|" options:0 metrics:0 views:viewDict]];

[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[randomLabel]-|" options:0 metrics:0 views:viewDict]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[randomLabel]-|" options:0 metrics:0 views:viewDict]];

Now you'll see that the contentView (and, therefore, the contentSize of the scrollView) are adjusted to fit the label with standard margins. And because I didn't specify the width/height of the label, that will adjust based upon the text you put into that label.


If you want the contentView to also adjust to the width of the main view, you could do redefine your viewDict like so, and then add these additional constraints (in addition to all the others, above):

UIView *mainView = self.view;

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView, contentView, randomLabel, mainView);

[mainView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[contentView(==mainView)]" options:0 metrics:0 views:viewDict]];

There is a known issue (bug?) with multiline labels in scrollviews, that if you want it to resize according to the amount of text, you have to do some sleight of hand, such as:

dispatch_async(dispatch_get_main_queue(), ^{
    randomLabel.preferredMaxLayoutWidth = self.view.bounds.size.width;
});
Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    I can't seem to get the label to expand vertically if the text is too long to fit on one line. I've set the number of lines to 0, and the lineBreakMode to NSLineBreakByWordWrapping. Am I missing something? – rdelmar May 30 '13 at 20:08
  • Additionally, I added the last line in your answer to pin the width of the contentView to the width of the screen (otherwise the contentView's width is wide enough to accommodate the long text). If I explicitly set the height of the label with a height constraint, then it works. – rdelmar May 30 '13 at 20:13
  • Yeah, Matt discovered [a couple of work-arounds](http://stackoverflow.com/questions/13149733/ios-autolayout-issue-with-uilabels-in-a-resizing-parent-view) for this issue/bug of multiline labels. I've added one of them to the bottom of my answer. A thoroughly dissatisfying solution. Looks like you have to choose between hardcoding values or this kludgy workaround. – Rob May 30 '13 at 20:41
  • 1
    Thanks for that link. I tried out Matt's work-arounds, and I think I like his other approach of subclassing the label better -- seems less kludgy. Especially if you're adding multiple labels, you can move the numberOfLines and setTranslates... methods into there as well as the layoutSubviews to make the code cleaner. I also noticed that I didn't get the proper heights of the labels on rotation using the dispatch_async method unless that code is put in viewWillLayoutSubviews (as Matt suggested), which means you need to create properties for your labels as well. – rdelmar May 30 '13 at 23:37
  • Do I have to create view programically? I wan't to have it all in one ViewController on the storyboard that is using autolayout, how do I achieve that? – Jacek Kwiecień Oct 28 '13 at 10:58
  • @Xylian No, you don't have to create it programmatically, though if you're doing the above in IB (in Xcode 5.0.1, at least), you need yet another view between the root view and the scroll view (because IB doesn't appear to let you do the leading, trailing, top, bottom alignment constraints between a scroll view's subview and the root view). Working with complex constraints in IB always seems to be this dance between one's objectives and the (seemly arbitrary) limitations IB places on the creation of constraints. – Rob Oct 28 '13 at 14:30
  • 1
    @Rob Thanks! Consider also linking to [Apple's technical note 2154 that explains this stuff in detail](https://developer.apple.com/library/ios/technotes/tn2154/_index.html). – ravron Sep 30 '14 at 01:42
1

I tried Rob's answer looks fine at first. BUT! if you also enabled zooming, this Autolayout code will get in the way. It keeps resizing the contentView when zooms. That is, if I zoomed in (zoomScale > 1), I won't be able to scroll the the parts outside the screen.

After days of fighting with Autolayout, sadly I couldn't find any solution. In the end, it doesn't even matter (wat? ok, sorry). In the end, I just remove Autolayout on contentView (use a fixed size contentView), and then in layoutSubviews, I adjust the self.scrollView.contentInset. (I'm using Xcode5, iOS 7)

I know this is not a direct solution to this question. But I just want to point out an easy workaround. It works perfectly for centering a fixed-size contentView in a scrollView, with just 2 lines of code! Hope it may help someone ;)

- (void)layoutSubviews {
    [super layoutSubviews];

    // move contentView to center of scrollView by changing contentInset,
    // because I CANNOT set this with AUTOLAYOUT!!!
    CGFloat topInset = (self.frame.size.height - self.contentView.frame.size.height)/2;
    self.scrollView.contentInset = UIEdgeInsetsMake(topInset, 0, 0, 0);
}
Hlung
  • 13,850
  • 6
  • 71
  • 90
-1

In general, Auto Layout considers the top, left, bottom, and right edges of a view to be the visible edges. That is, if you pin a view to the left edge of its superview, you’re really pinning it to the minimum x-value of the superview’s bounds. Changing the bounds origin of the superview does not change the position of the view.

The UIScrollView class scrolls its content by changing the origin of its bounds. To make this work with Auto Layout, the top, left, bottom, and right edges within a scroll view now mean the edges of its content view.

The constraints on the subviews of the scroll view must result in a size to fill, which is then interpreted as the content size of the scroll view. (This should not be confused with the intrinsicContentSize method used for Auto Layout.) To size the scroll view’s frame with Auto Layout, constraints must either be explicit regarding the width and height of the scroll view, or the edges of the scroll view must be tied to views outside of its subtree.

Note that you can make a subview of the scroll view appear to float (not scroll) over the other scrolling content by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview.

Here are two examples of how to configure the scroll view, first the mixed approach, and then the pure approach

Afzal Siddiqui
  • 24
  • 2
  • 12
  • Welcome to Stack Overflow. Please review [How do I write a good answer](https://meta.stackexchange.com/questions/7656/how-do-i-write-a-good-answer-to-a-question). – Rick Nov 28 '17 at 09:41