I've got a view that contains only a UILabel. This label contains multiline text. The parent has a variable width that can be resized with a pan gesture. My problem is that when I do this resizing the UILabel does not recalculate its height such that all of the content is still visible, it simply cuts it off.
I've managed to fix it with a bit of a hack but it is horribly slow to run:
- (void)layoutSubviews {
CGSize labelSize = [self.labelDescription sizeThatFits:CGSizeMake(self.frame.size.width, FLT_MAX)];
if (self.constraintHeight) {
[self removeConstraint:self.constraintHeight];
}
self.constraintHeight = [NSLayoutConstraint constraintWithItem:self.labelDescription attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:labelSize.height];
[self addConstraint:self.constraintHeight];
[super layoutSubviews];
}
Shouldn't this happen automatically with autolayout?
EDIT
The structure of my view is:
UIScrollView
---> UIView
---> UILabel
Here are the constraints on the UIScrollView:
<NSLayoutConstraint:0x120c4860 H:|-(>=32)-[DescriptionView:0x85c81c0] (Names: '|':UIScrollView:0x85db650 )>,
<NSLayoutConstraint:0x120c48a0 H:|-(32#900)-[DescriptionView:0x85c81c0] priority:900 (Names: '|':UIScrollView:0x85db650 )>,
<NSLayoutConstraint:0x120c48e0 H:[DescriptionView:0x85c81c0(<=576)]>,
<NSLayoutConstraint:0x120c4920 H:[DescriptionView:0x85c81c0]-(>=32)-| (Names: '|':UIScrollView:0x85db650 )>,
<NSLayoutConstraint:0x120c4960 H:[DescriptionView:0x85c81c0]-(32#900)-| priority:900 (Names: '|':UIScrollView:0x85db650 )>,
<NSLayoutConstraint:0x8301450 DescriptionView:0x85c81c0.centerX == UIScrollView:0x85db650.centerX>,
Here are the constraints on the UIView:
<NSLayoutConstraint:0x85c4580 V:|-(0)-[UILabel:0x85bc7b0] (Names: '|':DescriptionView:0x85c81c0 )>,
<NSLayoutConstraint:0x85c45c0 H:|-(0)-[UILabel:0x85bc7b0] (Names: '|':DescriptionView:0x85c81c0 )>,
<NSLayoutConstraint:0x85c9f80 UILabel:0x85bc7b0.trailing == DescriptionView:0x85c81c0.trailing>,
<NSLayoutConstraint:0x85c9fc0 UILabel:0x85bc7b0.centerY == DescriptionView:0x85c81c0.centerY>
The UILabel itself has no constraints on it, aside from pinning it to the edges of its parent
Okay, I finally nailed it. The solution is to set the preferredMaxLayoutWidth in viewDidLayoutSubviews, but only after the first round of layout. You can arrange this simply by dispatching asynchronously back onto the main thread. So:
- (void)viewDidLayoutSubviews {
dispatch_async(dispatch_get_main_queue(), ^{
self.theLabel.preferredMaxLayoutWidth = self.theLabel.bounds.size.width;
});
}
That way, you don't set preferredMaxLayoutWidth until after the label's width has been properly set by its superview-related constraints.
Working example project here:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch23p673selfSizingLabel4/p565p579selfSizingLabel/ViewController.m
EDIT: Another approach! Subclass UILabel and override layoutSubviews:
- (void) layoutSubviews {
[super layoutSubviews];
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
The result is a self-sizing label - it automatically changes its height to accommodate its contents no matter how its width changes (assuming its width is changed by constraints / layout).
I've fixed this issue after raising a bug with Apple. The issue that multiline text requires a two-pass approach to layout and it all relies on the property preferredMaxLayoutWidth
Here is the relevant code that needs to be added to a view controller that contains a multiline label:
- (void)viewWillLayoutSubviews
{
// Clear the preferred max layout width in case the text of the label is a single line taking less width than what would be taken from the constraints of the left and right edges to the label's superview
[self.label setPreferredMaxLayoutWidth:0.];
}
- (void)viewDidLayoutSubviews
{
// Now that you know what the constraints gave you for the label's width, use that for the preferredMaxLayoutWidth—so you get the correct height for the layout
[self.label setPreferredMaxLayoutWidth:[self.label bounds].size.width];
// And then layout again with the label's correct height.
[self.view layoutSubviews];
}
In Xcode 6.1 for iOS 7/8, I was able to get this to work by just setting preferredMaxLayoutWidth in a setter method that's called on my view to display the text for the label. I'm guessing it was set to 0 to begin with. Example below, where self.teachPieceLabel is the label. The label is wired up with constraints alongside other labels in a view in Interface Builder.
- (void)setTeachPieceText:(NSString *)teachPieceText {
self.teachPieceLabel.text = teachPieceText;
[self.teachPieceLabel setPreferredMaxLayoutWidth:[self.teachPieceLabel bounds].size.width];
}
Related
I've got a UITableViewController, with a custom view, with some subviews and controls inside, in the header of the table. Each one of these subviews are a custom subview (use a custom subview to draw corner radius with IBInspectable) and each have constraints for top, bottom, leading and trailing space (all set to 8) as well as for height (set to 60, 80 or 100, depending of each subview).
One of these subviews constraints can be modified programmatically in run time depending of user interaction. To do that, I create this two methods:
- (void)showSearchTypeTermField:(BOOL)animated
{
self.searchTypeHeightConstraint.identifier = #"Height 100";
[self.searchTypeHeightConstraint setConstant:100.0];
if (animated) {
[self.tableView.tableHeaderView layoutIfNeeded];
[UIView animateWithDuration:0.3 animations:^{
[self.tableView.tableHeaderView layoutIfNeeded];
}];
} else {
[self.tableView.tableHeaderView layoutIfNeeded];
}
self.searchTypeTermField.hidden = NO;
self.searchTypeTermField.text = #"";
[self.tableView.tableHeaderView systemLayoutSizeFittingSize:UILayoutFittingExpandedSize];
}
- (void)hideSearchTypeTermField:(BOOL)animated
{
self.searchTypeHeightConstraint.identifier = #"Height 60";
[self.tableView.tableHeaderView setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
[self.searchTypeHeightConstraint setConstant:60.0];
if (animated) {
[self.tableView.tableHeaderView layoutIfNeeded];
[UIView animateWithDuration:0.3 animations:^{
[self.tableView.tableHeaderView layoutIfNeeded];
}];
} else {
[self.tableView.tableHeaderView layoutIfNeeded];
}
self.searchTypeTermField.hidden = YES;
[self.tableView.tableHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
}
Because all of these subviews are in the table header view, every time I change the constraint, header should expand or compress depending of the case, thats why I use self.tableView.tableHeaderView systemLayoutSizeFittingSize.
The problem is I'm getting this error, when I call [self hideSearchTypeTermField:NO] the first time on viewdidLoad despite that visually everything seems fine:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7bf504e0 V:[SeachFormBackgroundView:0x7bea3ab0(60)]>",
"<NSLayoutConstraint:0x7bf51760 V:[SeachFormBackgroundView:0x7bf50690(60)]>",
"<NSLayoutConstraint:0x7bf53390 'Height 60' V:[SeachFormBackgroundView:0x7bf518e0(60)]>",
"<NSLayoutConstraint:0x7bf53ec0 V:[SeachFormBackgroundView:0x7bf535a0(80)]>",
"<NSLayoutConstraint:0x7bf54a80 V:[SeachFormBackgroundView:0x7bf54180(80)]>",
"<NSLayoutConstraint:0x7bf54eb0 V:|-(8)-[SeachFormBackgroundView:0x7bea3ab0] (Names: '|':UIView:0x7bea3a10 )>",
"<NSLayoutConstraint:0x7bf54f40 V:[SeachFormBackgroundView:0x7bea3ab0]-(8)-[SeachFormBackgroundView:0x7bf50690]>",
"<NSLayoutConstraint:0x7bf54f70 V:[SeachFormBackgroundView:0x7bf50690]-(8)-[SeachFormBackgroundView:0x7bf518e0]>",
"<NSLayoutConstraint:0x7bf55090 V:[SeachFormBackgroundView:0x7bf518e0]-(8)-[SeachFormBackgroundView:0x7bf535a0]>",
"<NSLayoutConstraint:0x7bf550f0 V:[SeachFormBackgroundView:0x7bf54180]-(8)-| (Names: '|':UIView:0x7bea3a10 )>",
"<NSLayoutConstraint:0x7bf55180 V:[SeachFormBackgroundView:0x7bf535a0]-(8)-[SeachFormBackgroundView:0x7bf54180]>",
"<NSAutoresizingMaskLayoutConstraint:0x7c149fc0 h=--& v=--& V:[UIView:0x7bea3a10(428)]>"
)
I'm really lost with this issue. What I'm doing wrong?
Thanks.
If you try adding the heights for the SeachFormBackgroundView instances (60 + 60 + 60 + 80 + 80 + 8 * 6 = 388 and their separators you'll see that they aren't equal to the parent UIView height (428). That's why you get this message.
You have to either adjust the constraints so that they add up to the parent view height, or resize the parent so that it's size matches the child constraints.
You don't need all of the constraints you have right now. Since you're setting a specific height to all of the subviews and spacing constraints, you only need to anchor them to the top or to the bottom of the superview.
Edit:
You don't need both of these constraints: V:|-(8)-[SeachFormBackgroundView:0x7bea3ab0] and V:[SeachFormBackgroundView:0x7bf54180]-(8)-|. The first one anchors your subviews to the top of the superview, and the scond one anchors them to the bottom of your superview. Remove one of them (I'd expect the bottom one) and the views will fall in place without throwing any AutoLayout exceptions.
When you set the header view the table view reads its size and sets a constraint for that height. Updating the constraints on the header view itself don't cause a reevaluation of that height. You should remove the header view, update its constraints, lay it out and then add the header view again.
I have a scrollview and a separate UIView where I placed a series of textFields and labels with constraints which fully occupies the top and bottom. I'm trying to adjust the UIView's height based on its subview constraints but it won't. What is happening is that the view keeps its height and force other textfields to collapse or shrink thus breaking the constraints.
Details
Each subview priority values :
compression = 750
hugging = 250
UIView priority values:
compression = 249
hugging = 749 Set to be lower than the rest.
Most of the textfields has aspect ratio constraint. This causes the field to adjust.
Each subview has vertical/top/bottom spacing between each other. The top and bottom elements has top and bottom constraints to the view as well.
What's on my code:
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
/* I had to adjust the UIView's width to fill the entire self.view.*/
if(![contentView isDescendantOfView:detailsScrollView]){
CGRect r = contentView.frame;
r.size.width = self.view.frame.size.width;
contentView.frame = r;
[detailsScrollView addSubview:contentView];
}
}
Screenshots
The view
This is what currently happens. In this instance it forces the email field to shrink. If I place a height value on it, it does not shrink but the layout engine finds another element to break
Edit:
Solved
Maybe I just needed some break to freshen up a bit. I did tried using constraints before but got no luck. However thanks to the suggestion I went back setting the constraints instead of setting the frame on this one and got it finally working.
Solution:
-(void)viewDidLoad{
[detailsScrollView addSubview:contentView];
[contentView setTranslatesAutoresizingMaskIntoConstraints:NO];
[detailsScrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(contentView,detailsScrollView);
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil
views:viewsDictionary];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:viewsDictionary];
NSArray *widthConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView(==detailsScrollView)]-0-|" options:0 metrics:nil views:viewsDictionary];
}
When you use interface builder to deal with the UIScrollView and its child UIView. usually a top, bottom, left and equal width constraints are set between the UIScrollView and its child which is the contentView in your case.
Without those constraints the other option is to set the content size of the UIScrollView. which was the way of using the UIScrollView before introducing constraints.
So, 1. you should add those constraints programmatically.
By using the constraints, the views frame is no longer needed to resize the views.
So, 2. remove frame setting for your content view.
I am not so happy with the way you set the frame in the viewDidLayoutMethod. if I am going to do that here I would take the frame setting out of the if statement.
The code would be as follow with no if statement:
[detailsScrollView addSubview:contentView];
// then set the constraints here after adding the subview.
Put this code anywhere but not inside your viewDidLayoutSubviews method. it will be a bigger problem than setting the frame in there inside if statement.
Note: Originally, if you are going to set frame in the viewDidLayoutSubviews
method. you should do it for all cases. for example for the if case
and the else case. because, next time this method is going to be
called the views will respond to the constraint. and lose its frame.
Another observation: if you want the view to response to its subviews constraint why you need to set the frame for it? right?
After adding the constraint you may need to call the method constraintNeedsUpdate or another related method.
I've got a UITableViewCell which contains a UIWebView. The table view cell adjusts it's height depending on the web view contents.
I've got it all working fine, however when the view loads, I get a constraint violation exception in the debugger (the app continues running and functionally works fine, but I'd like to resolve this exception if possible).
How I've got it set up:
The TableView sets the cell height like this:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == 0) {
[_topCell layoutIfNeeded];
CGFloat finalHeight = [_topCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return finalHeight + 1;
}
The cell constraints are as follows:
Arbitrary 7px offset from the cell's contentView (top) to the webView
Web view has arbitrary fixed height constraint of 62px (will expand later once content loads)
Arbitrary 8px offset from the webView to the cell's contentView (bottom)
in my viewDidLoad, I tell the webView to go and load a URL, and in the webViewDidFinishLoad, I update the web view height constraint, like this
-(void)webViewDidFinishLoad:(UIWebView *)webView {
CGSize fittingSize = [webView sizeThatFits:CGSizeZero];
// fittingSize is approx 500
[self.tableView beginUpdates];
// Exceptions happen on the following line setting the constant
_topCell.webViewHeightConstraint.constant = fittingSize.height;
[_topCell layoutSubviews];
[self.tableView endUpdates];
}
The exception looks like this:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x10964b250 V:[webView(62)] (Names: webView:0x109664a00 )>",
"<NSLayoutConstraint:0x109243d30 V:|-(7)-[webView] (Names: webView:0x109664a00, cellContent:0x1092436f0, '|':cellContent:0x1092436f0 )>",
"<NSLayoutConstraint:0x109243f80 V:[webView]-(8)-| (Names: cellContent:0x1092436f0, webView:0x109664a00, '|':cellContent:0x1092436f0 )>",
"<NSAutoresizingMaskLayoutConstraint:0x10967c210 h=--& v=--& V:[cellContent(78)] (Names: cellContent:0x1092436f0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x10964b250 V:[webView(62)] (Names: webView:0x109664a00 )>
This seems a bit weird. It's implied that the constraint which sets the height of the web view is going to be broken, however the web view does get it's height correctly set, and the tableview renders perfectly well.
From my guesses, it looks like the newly increased web view height constraint (it's about 500px after the web view loads) is going to conflict with the <NSAutoresizingMaskLayoutConstraint:0x10967c210 h=--& v=--& V:[cellContent(78)] setting the cell height to 78 (put there by interface builder). This makes sense, however I don't want that cell content to have a fixed height of 78px, I want it to increase it's height, and functionally, it actually does this, just with these exceptions.
I've tried setting _topCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; to attempt to remove the NSAutoresizingMaskLayoutConstraint - this stops the exceptions, but then all the other layout is screwed up and the web view is about 10px high in the middle of the table view for no reason.
I've also tried setting _topCell.contentView.autoresizingMask |= UIViewAutoresizingFlexibleHeight; in the viewDidLoad to hopefully affect the contentView 78px height constraint, but this has no effect
Any help would be much appreciated
An alternative answer, which ends up being far simpler:
Set the priority of the webViewHeight constraint to something other than required. Works well and there are no warnings. I'd recommend going with this :-)
I worked out a solution. It seems like a hack, but it works correctly and produces no errors and warnings.
The trick was to dissociate the web view's layout and height from the contentView.
I did this as follows:
In the tableviewcell, add a new UIView, called containerView
Give it constraints for Top:0 Left:0 Right:0 but no bottom constraint and no height constraint
Put the web view (and any other content) inside this containerView
Add constraints such that the web view is 7px from the top and bottom of containerView, and has a height constraint of 62px
At this point, the containerView isn't connected to the bottom of the table view cell, and so it could over/under flow without auto-layout complaining.
Now, in tableView:heightForRowAtIndexPath:, we calculate the cell height based on the containerView as follows:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == 0) {
CGFloat finalHeight = [_topCell.containerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return finalHeight + 1;
}
return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}
All done!
From the Constraint log
The Constraint you are trying to achieve is as below,
In a 78 px cellContentHeight you want to fit a content of 7(top space) + 62 (Web View) + 8 (bottom space). which is 77 (not equal to 78). Either of two option would work,
Try giving top margin as 8 (instead of 7) and make it 78. (V:|-8-[WebView(62)]-8-|)
Attach to the top, give 8 pixel top space (V:|-8-[WebView(62)]) and don't specify 8 px bottom space.
Attach to Bottom, give 8 pixel bottom space (V:[WebView(62)-8-|) and don't specify top space.
I'm working with a subclassed UITableViewCell and I need to align a UILabel's text to the Top Left all while using Auto Layout. I realize reading up that sizeToFit really shouldn't be used with Auto Layout and I'd like to avoid it, and somehow use a constraint. Basically the label's text is reset every reuse of the cell so the sizing would need to be dynamic on reuse.
Here's the Lazy initializer label inside the subclassed cell:
- (UILabel *)commentsLabel {
if (_commentsLabel == nil) {
_commentsLabel = [[UILabel alloc] init];
[_commentsLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
_commentsLabel.numberOfLines = 0;
_commentsLabel.lineBreakMode = NSLineBreakByWordWrapping;
_commentsLabel.preferredMaxLayoutWidth = 100;
}
return _commentsLabel;
}
There are auto-layout constraints being set on the label (commentsLabel is a subView added to self.customView on the cell subclass):
[self.customView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-locationToTopPadding-[locationLabel(locationHeight)]-locationToCommentsPadding-[commentsLabel]-commentsToBottomPadding-|"
options:0
metrics:#{
#"locationToTopPadding":#(locationToTopPadding),
#"locationHeight":#(locationHeight),
#"locationToCommentsPadding":#(locationToCommentsPadding),
#"commentsToBottomPadding":#(commentsToBottomPadding)
}
views:viewsDictionary]];
[self.customView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[thumbnailImageView]-[commentsLabel]-|"
options:0
metrics:#{#"commentsWidth":#(commentsWidth)}
views:viewsDictionary]];
setting just:
self.commentsLabel.preferredMaxLayoutWidth = 100;
doesn't seem to work although that is mentioned in most answers.
currently have this implemented but not seeing any results.
UILabel sizeToFit doesn't work with autolayout ios6
another answer I tried.
UILabel sizeToFit only works with AutoLayout turned off
I feel that I'm just missing 1 constraint that can be added in addition to the constraint above but the programmatic constraints I try to add don't work or throw an exception. I'm working completely in code there are no xibs.
ETA: tried setting a height constraint inside the existing vertical constraint line:
like suggested here: Dynamically change UILabel width not work with autolayout
in the vertical constraint line above making it:
-[commentsLabel(>=30#900)]-
I've messed around with the height value and priority value and nothing changed.
ETA: making some progress, I think it has to do with the bottom padding of the label, tried this and some of the labels are aligned correctly some aren't:
->=commentsToBottomPadding-
solution in the event any one else runs into the same issue with a UITableViewCell and a custom Label that changes dynamically:
-[commentsLabel]->=yourBottomPadding-
put that in the vertical constraint
call this in the viewController after you set the text:
[cell updateConstraints];
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
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!
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).
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.
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;
});
}
At every moment the scroll view should know its content size. The content size is inferred from the scrollview's subviews. It is very handy to map controller properties to the constraints in the xib file describing heights of the subviews. Then in the code (an animation block) you can just change constants of these constraint properties. If you need to change the entire constraint, keep a reference to it, so that you can update it later in the parent container.
My variant for scroll view with !Dynamic! height:
1) Add scroll view to your UIView. Pin all (top, bottom, lead, trail) constraints.
2) Add UIView to Scroll View. Pin all (top, bottom, lead, trail) constraints. It will be your Content view. You can also rename it.
3) Control drag from Content view to Scroll view - Equal width
4) Add content to your UIView. Set needed constraints. And! At the lower item add bottom constraint NOT Greater or equal (>=)(Like most people talks) BUT Equal! Set it to 20 for example.
In my situation I have UIImageView in content. I have connected it's height to code. And if I change it to like 1000, scroll is visible. And all works.
Works like a charm for me. Any questions - welcome to comments.