Auto layout UIScrollView with subviews with dynamic heights
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
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.