Auto Layout Uiscrollview with Subviews with Dynamic Heights

Auto layout UIScrollView with subviews with dynamic heights

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!

Make UIScrollView with dynamic height subviews work automatically with Auto Layout

Ok, I was doing three things wrong:

1st: I was calculating the height of the labels to set then the size of the "labels container" UIView using a height constraint. Height constraints not needed as Auto Layout manages everything.

2nd: I was modifying the "C" UIView constraint height manually by adding the height value of each "labels container" UIView added.

3rd: After fixing the first two steps, I forgot to set the last added UIView constraint with its parent view...

Now everything works as expected.

UIScrollView ContentView dynamic height constraints

Do it this way:

  1. Add the scroll view to your view controller and do not set any constraints to it.
  2. Start adding your subviews (images, labels etc) in your scroll view and set the desired constraints. For labels, you don't need to set the height, as they will auto-resize their height depending on content. For other subviews, also set the height or other constraints that will later determine the height (i.e. aspect ratio). If your scroll view gets too small for all the subviews you want to add, drag from its bottom to increase its height. It's important to keep all the subviews inside the scroll view and chain them one to another.
  3. For the last label in the scroll view, add a bottom constraint to the scroll view.
  4. Add margin constraints from your scroll view to the main view (top, leading, bottom and trailing).

Since all subviews are chained and pinned to top and bottom inside the scroll view and the scroll view is pinned to the storyboard's view, this will work.

Do not set the height of the scroll view. Setting it to 757 px is wrong because it renders your scroll view below the screen on devices with a height lower than 757px.

UIScrollView with dynamically sized content

When adding multiple elements to a scroll view at run-time, you may find it much easier to use a UIStackView... when setup properly, it will automatically grow in height with each added object.

As a simple example...

1) Start by adding a UIScrollView (I gave it a blue background to make it easier to see). Constrain it to Zero on all 4 sides:

Sample Image

Note that we see the "red circle" indicating missing / conflicting constraints. Ignore that for now.

2) Add a UIView as a "content view" to the scroll view (I gave it a systemYellow background to make it easier to see). Constrain it to Zero on all 4 sides to the Content Layout Guide -- this will (eventually) define the scroll view's content size. Also constrain it equal width and equal height to the Frame Layout Guide:

Sample Image

Important Step: Select the Height constraint, and in the Size Inspector pane select the Placeholder - Remove at build time checkbox. This will satisfy auto-layout in IB during design time, but will allow the height of that view to shrink / grow as necessary.

3) Add a Vertical UIStackView to the "content view". Constrain it to Zero on all 4 sides. Configure its properties to Fill / Fill / 8 (as shown below):

Sample Image

4) Add an @IBOutlet connection to the stack view in your view controller class. Now, at run-time, as you add UI elements to the stack view, all of your "scrollability" will be handled by auto-layout.

Here is an example class:

class DynaScrollViewController: UIViewController {

@IBOutlet var theStackView: UIStackView!

override func viewDidLoad() {
super.viewDidLoad()

// local var so we can reuse it
var theLabel = UILabel()
var theImageView = UIImageView()

// create a new label
theLabel = UILabel()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theLabel.translatesAutoresizingMaskIntoConstraints = false
// multi-line
theLabel.numberOfLines = 0
// cyan background to make it easy to see
theLabel.backgroundColor = .cyan
// add 9 lines of text to the label
theLabel.text = (1...9).map({ "Line \($0)" }).joined(separator: "\n")

// add it to the stack view
theStackView.addArrangedSubview(theLabel)

// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .yellow
// add 5 lines of text to the label
theLabel.text = (1...5).map({ "Line \($0)" }).joined(separator: "\n")

// add it to the stack view
theStackView.addArrangedSubview(theLabel)

// create a new UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load an image for it - I have one named background
if let img = UIImage(named: "background") {
theImageView.image = img
}
// let's give the image view a 4:3 width:height ratio
theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 4.0/3.0).isActive = true

// add it to the stack view
theStackView.addArrangedSubview(theImageView)

// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .green
// add 2 lines of text to the label
theLabel.text = (1...2).map({ "Line \($0)" }).joined(separator: "\n")

// add it to the stack view
theStackView.addArrangedSubview(theLabel)

// add another UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load a different image for it - I have one named AquariumBG
if let img = UIImage(named: "AquariumBG") {
theImageView.image = img
}
// let's give this image view a 1:1 width:height ratio
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor, multiplier: 1.0).isActive = true

// add it to the stack view
theStackView.addArrangedSubview(theImageView)

}

}

If the steps have been followed, you should get this output:

Sample Image

and, after scrolling to the bottom:

Sample Image

CustomView with Dynamic ScrollView ContentSize Autolayout called in addSubview

Couple of issues.

First, you are setting the frame of customV to the bounds of wantToShowHereView -- but you're doing so in viewDidLoad(). That bounds will almost certainly change between viewDidLoad() and the time you actually see it on screen (device size, orientation, etc).

Second, customV is a UIView onto which you are adding the XIB's "root view" (which contains your scroll view) as a subview to customV ... but you're not setting any constraints (or other resizing behaviors) on that view.

Third, you're mixing relative constraints with absolute constraints (widths, heights, leading, trailing, etc), which will again cause issues when the overall frame changes... and you're explicitly setting the frame of customV instead of adding constraints at run-time.

You can get a start on fixing things:

Step one - remove customV instantiation from viewDidLoad().

Step two - add the following viewDidAppear() method.

- (void)viewDidAppear:(BOOL)animated {

[super viewDidAppear:animated];

customView *customV = [[customView alloc] initWithFrame:self.wantToShowHereView.bounds];
customV.view.frame = customV.bounds;
[self.wantToShowHereView addSubview:customV];

}

Doing only that should give you a properly scrollable view.

What you probably want to do, though, is keep the init in viewDidAppear() but add constraints there to make use of auto-layout.

Also, I'd recommend re-working the constraints on the elements in your customView.xib so the scrolling (the contentSize) is determined by the actual content of your scroll view, not by hard-coding a height of your contentView.


Edit:

Here is how your viewDidLoad could look (in ViewController.m):

- (void)viewDidLoad
{
[super viewDidLoad];

customView *customV = [[customView alloc] initWithFrame:self.wantToShowHereView.bounds];
[self.wantToShowHereView addSubview:customV];

customV.translatesAutoresizingMaskIntoConstraints = NO;

[customV.topAnchor constraintEqualToAnchor:self.wantToShowHereView.topAnchor].active = YES;
[customV.bottomAnchor constraintEqualToAnchor:self.wantToShowHereView.bottomAnchor].active = YES;
[customV.leadingAnchor constraintEqualToAnchor:self.wantToShowHereView.leadingAnchor].active = YES;
[customV.trailingAnchor constraintEqualToAnchor:self.wantToShowHereView.trailingAnchor].active = YES;

}

and setup in customView.m:

- (void)setup {

[[NSBundle mainBundle] loadNibNamed:@"customView" owner:self options:nil];
[self addSubview:self.view];

self.view.translatesAutoresizingMaskIntoConstraints = NO;

[self.view.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
[self.view.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
[self.view.leadingAnchor constraintEqualToAnchor:self.leadingAnchor].active = YES;
[self.view.trailingAnchor constraintEqualToAnchor:self.trailingAnchor].active = YES;

NSLog(@"contentSize Height :%f", self.myscrollview.contentSize.height);
NSLog(@"contentView Height :%f", self.contentView.frame.size.height);

}

Adding a dynamically sized view (height) to a UIScrollView with Auto Layout

In the structure you are presenting, using AutoLayout, the contentSize of your UIScrollView is driven by the intrinsicContentSize of its subviews, here your SinglePostView.

The problem is, SinglePostView being a subclass of UIView, its intrinsicContentSize is always CGSizeZero when considered by itself. What you need to do is make the intrinsicContentSize of your SinglePostView depend on the intrinsicContentSize of its subviews.

Then, because the subviews of your SinglePostView are UILabels, and because a UILabel's intrinsicContentSize is the smallest size it needs to display its content, your SinglePostView's intrinsicContentSize will be equal to the sum of its subviews intrinsicContentSizes, that is the total size needed to display the content of all three of your labels.

Here is how to do it.

Step 1: Removing all automatically set constraints

First, as you partially did, you need to remove all constraints automatically set by the system.

Assuming you don't have any constraints set in your storyboard or XIB (or you don't even have one of these), just do:

scrollView.translatesAutoresizingMaskIntoConstraints  = NO;
singlePost.translatesAutoresizingMaskIntoConstraints = NO;
titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
contentLabel.translatesAutoresizingMaskIntoConstraints = NO;

Now you have a clear slate and you can start setting your own constraints.

Step 2: Constraining the scrollView

First, let's create, as you did, the views references dictionary for AutoLayout to use:

NSDictionary *views = NSDictionaryOfVariableBindings(scrollView, singlePost, titleLabel, subtitleLabel, contentLabel);

Then, also as you already did, let's constrain the scroll view to be the size of its superview:

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

Step 3: Constraining the SinglePostView to push the scrollView's contentSize

For this step to be clear, you have to understand that every constraints set between a UIScrollView and its subviews will actually change the contentSize of the UIScrollView, not its actual bounds. For Example, if you constrain a UIImageView to the borders of its parent UIScrollView and then put an image twice the size of the UIScrollView inside the UIImageView, your image won't get shrunk, its the UIImageView that will take the size of its image and become scrollable inside the UIScrollView.

So here is what you have to set here:

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[singlePost]-0-|" options:0 metrics:0 views:views]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[singlePost]-0-|" options:0 metrics:0 views:views]];
[scrollView addConstraint:[NSLayoutConstraint constraintWithItem:singlePost
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:scrollView
attribute:NSLayoutAttributeWidth
multiplier:1.0f
constant:0.0f]];

First two constraints are pretty obvious. The third one, however, is here because, for your UILabels to display their content properly and still be readable, you will probably want them to be multilined and the scrolling to be vertical, not horizontal. That's why you set your SinglePostView's width to be the same as your scrollView's. This way, you prevent your scrollView's contentSize.width to be anything more than its bounds.width.

Step 4: Constraining your UILabels to "push" the bounds of your SinglePostView

Fourth and final step, you now need to set constraints on your SinglePostView's subviews, so that it gets an intrinsicContentSize from them.

Here is how you do it (simplest implementation, no margins, one label after the other vertically):

[singlePost addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[titleLabel]-0-|" options:0 metrics:0 views:views]];
[singlePost addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[subtitleLabel]-0-|" options:0 metrics:0 views:views]];
[singlePost addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[contentLabel]-0-|" options:0 metrics:0 views:views]];
[singlePost addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[titleLabel]-0-[subtitleLabel]-[contentLabel]-0-|" options:0 metrics:0 views:views]];

And with that you should be done.

One last advice, you should definitely look into UIStoryboard to do these kinds of things. It's a lot simpler and more visual.

Hope this helps,

P.S.: If you want, I can take some time and push a project using both UIStoryboard and the Visual Format Language on Github. Just tell me if you would need one.

Good luck.

How to use scrollview with subviews of a dynamic height?

Quick answer

  1. Remove the current height constraint on your label.
  2. Ensure the labels Lines property is set to 0 and Line Breaks is set to Word Wrap.
  3. Add vertical spacing constraints to the views above and below the label.
  4. Ensure that every view has vertical spacing constraints from the top to bottom margins, in order for the scroll view to infer the height of its contentView.

Explanation

In order for the scroll view to infer its content size it must have constraints from margin to subviews to margin - imagine it like a balloon the content is the air inside that pushes on the wall to make the balloon the size it is. The constraints from the subviews to margins allow the size of the subviews to push the walls of the content view out.

For the label setting the Lines property to 0 means it will have a variable amount of lines just as you want. The Line Breaks property being set to Word Wrap means it will ensure words are not cut off (truncated) or broken up into characters and instead pushed onto the next line as whole words.



Related Topics



Leave a reply



Submit