Auto layout UIScrollView with subviews with dynamic heights

58,497

Solution 1

I use pure structure like the following

-view
  -scrollView
    -view A
    -view B
    -Button

Make sure Button(THE LAST view) has a constraint(vertical spacing from its bottom to superview, which is the scrollview), in this case, no matter what changes for your view A and view B would be, scrollView's height will be changed accordingly.

I reference to this great online book site.

Just read the "Creating a scroll view" section, you should have an idea.

I had the similar problem that I was creating a detail view and using Interface Builder with Auto layout is such a good fit for the task!

Good luck!

(Additional resources:

Stack overflow discussion about the auto layout for scroll view.

iOS 6 has a Release Notes talking about Auto Layout support for UIScrollView.

Free online iOS book explanation about scroll view. This actually helped me a lot!

Solution 2

Let's say we have a hierachy like this (Label1 is a subview of ContentView; ContentView is a subview of ScrollView, ScrollView is a subiview of the viewcontroller's view):

ViewController's View ScrollView ContentView Label1 Label2 Label3

ScrollView is constrained with autolayout in the normal way to the viewcontroller's view.

ContentView is pinned top/left/right/bottom to scrollview. Meaning you have constraints that make the ContentView's top/bottom/leading/trailing edges constrained to be equal to the same edges on the ScrollView. Here is a key: these constraints are for the contentSize of the ScrollView, not its frame size as shown in the viewcontroller's view. So it's not telling the ContentView to be the same frame size as the displayed ScrollView frame, it's rather telling Scrollview that the ContentView is its content and so if contentview is larger than the ScrollView frame then you get scrolling, just like setting scrollView.contentSize larger than scrollView.frame makes the content scrollable.

Here is another key: now you have to have enough constraints between ContentView, Label1-3, and anything else besides the Scrollview for the ContentView to be able to figure out it's width and height from those constraints.

So for example if you want a vertically scrolling set of labels, you set a constraint to make the ContentView width equal to the ViewController View's width, that takes care of the width. To take care of the height, pin Label1 top to ContentView top, Label2 top to Label1 bottom, Label3 top to Label2 bottom, and finally (and importantly) pin Label3's bottom to ContentView's bottom. Now it has enough information to calculate the ContentView's height.

I hope this gives someone a clue, as I read through the above posts and still couldn't figure out how to make the ContentView's width and height constraints properly. What I was missing was pinning the Label3's bottom to the ContentView's bottom, otherwise how could ContentView know how tall it is (as Label3 would just then be floating, and there would be no constraint to tell ContentView where it's bottom y position is).

Solution 3

This is an example of how I have laid out a pure autolayout UIScrollView with a container view. I've commented to make it clearer:

container is a standard UIView and body is a UITextView

- (void)viewDidLoad
{
    [super viewDidLoad];

    //add scrollview
    [self.view addSubview:self.scrollView];

    //add container view
    [self.scrollView addSubview:self.container];

    //body as subview of container (body size is undetermined)
    [self.container addSubview:self.body];

    NSDictionary *views = @{@"scrollView" : self.scrollView, @"container" : self.container, @"body" : self.body};
    NSDictionary *metrics = @{@"margin" : @(100)};

    //constrain scrollview to superview, pin all edges
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:kNilOptions metrics:metrics views:views]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:kNilOptions metrics:metrics views:views]];

    //pin all edges of the container view to the scrollview (i've given it a horizonal margin as well for my purposes)
    [self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[container]|" options:kNilOptions metrics:metrics views:views]];
    [self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[container]-margin-|" options:kNilOptions metrics:metrics views:views]];

    //the container view must have a defined width OR height, here i am constraining it to the frame size of the scrollview, not its bounds
    //the calculation for constant is so that it's the width of the scrollview minus the margin * 2
    [self.scrollView addConstraint:[NSLayoutConstraint constraintWithItem:self.container attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:-([metrics[@"margin"] floatValue] * 2)]];

    //now as the body grows vertically it will force the container to grow because it's trailing edge is pinned to the container's bottom edge
    //it won't grow the width because the container's width is constrained to the scrollview's frame width
    [self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[body]|" options:kNilOptions metrics:metrics views:views]];
    [self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[body]|" options:kNilOptions metrics:metrics views:views]];
}

In my example 'body' is a UITextView, but it could be anything else. If you happen to be using a UITextView as well note that in order for it to grow vertically it must have a height constraint that gets set in viewDidLayoutSubviews. So add the following constraint in viewDidLoad and keep a reference to it:

self.bodyHeightConstraint = [NSLayoutConstraint constraintWithItem:self.body attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:nil multiplier:1.0f constant:100.0f];
[self.container addConstraint:self.bodyHeightConstraint];

Then in viewDidLayoutSubviews calculate the height and update the constraint's constant:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    [self.bodyHeightConstraint setConstant:[self.body sizeThatFits:CGSizeMake(self.container.width, CGFLOAT_MAX)].height];

    [self.view layoutIfNeeded];
}

The second layout pass is needed to resize the UITextView.

Solution 4

Use this code. ScrollView setContentSize should be called async in main thread.

Swift:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        var contentRect = CGRect.zero

        for view in self.scrollView.subviews {
           contentRect = contentRect.union(view.frame)
        }

        self.scrollView.contentSize = contentRect.size
    }
}

Objective C:

 - (void)viewDidLayoutSubviews {
     [super viewDidLayoutSubviews];

     dispatch_async(dispatch_get_main_queue(), ^ {
                        CGRect contentRect = CGRectZero;

                        for(UIView *view in scrollView.subviews)
                           contentRect = CGRectUnion(contentRect,view.frame);

                           scrollView.contentSize = contentRect.size;
                       });
}
Share:
58,497
lukasz
Author by

lukasz

Updated on July 09, 2022

Comments

  • lukasz
    lukasz almost 2 years

    I'm having troubles with UIScrollView using auto layout constraints. I have the following view hierarchy, with constraints set through IB:

    - ScrollView (leading, trailing, bottom and top spaces to superview)
    -- ContainerView (leading, trailing, bottom and top spaces to superview)
    --- ViewA (full width, top of superview)
    --- ViewB (full width, below ViewA)
    --- Button (full width, below ViewB)
    

    The ViewA and ViewB have initial heights of 200 points, but it can be expended vertically to an height of 400 points by clicking on it. ViewA and ViewB are expanded by updating their height constraint (from 200 to 400). Here is the corresponding snippet :

    if(self.contentVisible) {
        heightConstraint.constant -= ContentHeight;
        // + additional View's internal constraints update to hide additional content 
        self.contentVisible = NO;
    } else {
        heightConstraint.constant += ContentHeight;
        // + additional View's internal constraints update to show additional content
        self.contentVisible = YES;
    }
    
    [self.view setNeedsUpdateConstraints];
    [UIView animateWithDuration:.25f animations:^{
        [self.view layoutIfNeeded];
    }];
    

    My problem is that if both views are expanded, I need to be able to scroll to see the whole content, and right now the scroll is not working. How can I manage to update the scroll view using constraints to reflect the changes of ViewA and ViewB heights ?

    The only solution I can think of so far is to manually set the height of the ContainerView after the animation, which will be the sum of the heights of ViewA + ViewB + Button. But I believe there is a better solution?

    Thanks

  • lukasz
    lukasz almost 11 years
    Thanks for the resources! Small change of plan, my client wanted an iOS 5 support so I had to switch off auto layout, but I will come back to this later.
  • FDIM
    FDIM about 10 years
    "Make sure Button(THE LAST view) has a constraint(vertical spacing from its bottom to superview, which is the scrollview)" <-- I love this part!
  • dsmDev
    dsmDev about 10 years
    Nice answer. My problem was having the dynamically sized label inside a UIView that was a sub view of the scroll view. Following the structure shown here I took the label out of the UIView, which I deleted. I set up the constraints on the label and it worked fine with my code setting the scroll view's content size to the label's text size.
  • Recycled Steel
    Recycled Steel almost 9 years
    This part made it clear for me "So it's not telling the ContentView to be the same frame size as the displayed ScrollView frame, it's rather telling Scrollview that the ContentView is its content".
  • Pangu
    Pangu over 8 years
    Thanks Doug. Your "if you want a vertically scrolling set of [ ]....you set a constraint to make the ContentView width equal to the ViewController View's width, that takes care of the width" fixed a lot of headache for me. I was setting both the ContentView's width and height equal to the ViewController's view. I deleted the equal height and that fixed a lot of constraints issues. Thanks!
  • Developer
    Developer about 8 years
    Come with some example or code. Its really very easy to put your own understanding in words.
  • Berat Cevik
    Berat Cevik over 7 years
    Great answer, nobody mentions "pin Label3's bottom to ContentView's bottom". Saved my life!
  • jshapy8
    jshapy8 almost 7 years
    What if the bottom item is also dynamically growing?
  • Vlad Pulichev
    Vlad Pulichev almost 7 years
    @jshapy8 I'm not sure, but I think, that it will resize UIView in scroll view. And scrollview will adapt its height. You can try it yourself. I cant do it atm
  • jshapy8
    jshapy8 almost 7 years
    Yes, I’ve found that it does resize. I think manually redrawing the frame of the dynamic view(s) and the view(s) below it is the tricky part, not really just the constraints
  • Vlad Pulichev
    Vlad Pulichev almost 7 years
    @jshapy8 yeah, I'am resizing it too