iOS Autolayout: How to move views when label text gets longer - ios

I have a tableview cell with two labels and a small image.
If the labeltexts are short, all fits in one row like this:
.________________________________________.
| |
| [firstlabel] [img] [secondlabel] > |
|________________________________________|
However, if the labeltexts are getting longer, the image
and the second label should move to the second line:
.________________________________________.
| |
| [firstveryverylonglabel] > |
| [img] [secondverylonglabel] |
|________________________________________|
How would I do this with autolayout?
The first label is easy - just add constraints for left and top. Labels are always only one line heigh, and get truncated in the middle if the text gets way too long.
The image must always be in front of the second label (default space) horizontally, and both centered vertically. The image size should scale with the dynamic text font size (preferredFontForTextStyle:UIFontTextStyleBody) of the labels, so that if the user chooses large text the image also gets drawn larger.
The cell height should of course grow if two lines are needed.
Bonus Points if setting the necessary constraints would be possible in Interface Builder, but if not then I could set them with code in the init routine of the tableview cell.
My app should run under iOS 7 and later - no need for iOS 6. So I must compute the height in tableview:heightForCellAtIndexPath:, and will use a hidden cell to let Autolayout do its magic there.

I ended with setting constraints in code. I embedded the labels and the image in another view, then implemented -updateConstraints for the cell and the view.
In -tableView:heightForRowAtIndexPath:, I first let Autolayout make a first pass on the static hidden cell (just used for measuring) to set up the contentView by calling layoutIfNeeded, then force it to update the constraints by calling setNeedsUpdateConstraints followed by updateConstraintsIfNeeded, and finally measure the height of the cell with systemLayoutSizeFittingSize:UILayoutFittingCompressedSize.
In my view's updateConstraints method, I first try to arrange the cells side-by-side with
[self removeConstraints];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
#"V:|-margin-[first]-margin-|"
options:0 metrics:metrics views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
#"H:|-margin-[first]-[image]-[second]-margin-|"
options:NSLayoutFormatAlignAllCenterY metrics:metrics views:views]];
Then I call systemLayoutSizeFittingSize:UILayoutFittingCompressedSize on the view and compare the width to the maximum possible space inside the contentView, given its fixed horizontal start position. If it's too wide, I need to set a two-line constraint set:
[self removeConstraints];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
#"V:|-margin-[first]-[second]-margin-|"
options:0 metrics:metrics views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
#"H:|-margin-[first]-margin-|"
options:0 metrics:metrics views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
#"H:|-margin-[image]-[second]-margin-|"
options:NSLayoutFormatAlignAllCenterY metrics:metrics views:views]];
In the cell's updateConstraints method, I just call setNeedsUpdateConstraints on the view to make sure that the view is recalculated after the cell's size changed (e.g. when rotating portrait<=>landscape).
And that's it.

Related

Dynamic UILabel size iOS 7 issue

I'm trying resize a label dynamically according to text height. The height can vary from 0 to many lines in the UILabel. I've come up with a solution for this problem that works fine on iOS 8 but fails on iOS 7.1 which I'm trying to support as well.
Autolayout is not being used in this project and all constraints are done programatically.
The code is as follows:
//TableDelegate.m
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 85.0f;
}
//CustomTableViewCell.m
-(UILabel *)commentTextLabel
{
if(!_commentTextLabel)
{
_commentTextLabel = [UILabel new];
_commentTextLabel.numberOfLines = 0;
_commentTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
}
return _commentTextLabel;
}
-(void)setupViews
{
[self.contentView addSubview:self.profilePictureView];
[self.contentView addSubview:self.userName];
[self.contentView addSubview:self.timePublishedLabel];
[self.contentView addSubview:self.commentTextLabel];
[self.contentView addSubview:self.seeMoreButton];
self.backgroundColor = [UIColor salooteInputTextBg];
self.contentView.backgroundColor = [UIColor salooteInputTextBg];
NSDictionary *views = #
{
#"picture" : self.profilePictureView,
#"userName" : self.userName,
#"timePublished" : self.timePublishedLabel,
#"text" : self.commentTextLabel,
#"seeMore" : self.seeMoreButton
};
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[picture(38)]-5-[userName]-5-[timePublished]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[picture]-5-[text]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[seeMore]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-5-[userName]-5-[text]-5-[seeMore]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-5-[picture(38)]" options:0 metrics:nil views:views]];
}
-(void)updateConstraints
{
[super updateConstraints];
}
iOS 8 result (left) iOS 7.1 result (right)
I'm not setting any height constraint in my code for the UILabel but rather trying to let the constraints adjust the vertical height for me. If anyone has some input on how to make this work properly on iOS 7.1 I would really appreciate it.
Moving constraints into setupViews produces this: (iOS 7.1 top iOS 8 bottom)
It seems to me you're not adding vertical constraints to the commentTextLabel? You only have this:
//Comment text
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[picture]-5-[text]-0-|" options:0 metrics:nil views:views]];
Try setting a vertical constraint as well--it's likely that you're getting insufficient constraints errors and iOS 8 is guessing the height better than iOS 7. Also, if you're adding constraints to the views, you shouldn't have to call sizeToFit inside the getter.
Autolayout is not being used in this project and all constraints are done programatically.
You're still using Autolayout even if you're adding the constraints only programatically. :)
In response to edits
Your vertical height constraint is insufficient--you only specified the height of the commentTextLabel but not its y-coordinate. Remember that the main objective in Autolayout is to provide a complete set of constraints such that iOS can compute for a view's x, y, width, and height.
I think your constraints are screwed up overall. :) Try adding these rules to the content view instead (I just used 5 for any padding):
H:|-5-[picture(38)]-5-[username]-5-[timePublished]-5-|
H:[picture]-5-[text]-5-|
H:|-5-[seeMore]-5-|
V:|-5-[username]-5-[text]-5-[seeMore]-5-|
V:|-5-[picture(38)]
Also, add your constraints in setupViews--you should only have to add your constraints once and ONLY modify them in updateConstraints. I think updateConstraints is called every time layoutSubviews is called so your constraints keep getting added every time the cell's layout is refreshed.
In response to edits
Your label's word wrap style must be set, too. From inside the commentTextLabel, add
_commentTextLabel.lineBreakMode = NSLineBreakByWordWrapping;
Always set that in conjunction to numberOfLines = 0 if you want a UILabel with a dynamic height.
You also need to right-align your seeMore label (it occupies the full width of the cell minus the padding) by setting that label's alignment property.
And try providing a bigger faux height for now--perhaps 150 or 200 instead of 85, just so we can see all the elements.
For the timePublished label, I forgot to indicate the following vertical constraint:
V:|-5-[timePublished]
I have found that the only way to support both iOS7 and iOS8 easily, is to do the height calculations for each cell yourself using off screen prototypes. The following is an excellent article on the issues. I could find no way to mix auto layout height calculation from iOS8 with manual height estimates for iOS7 in a single code base.
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
The only issue I had with this method was when I used size classes to change the cell font sizes so I could have larger font on iPad etc... This issue is discussed here:
Offscreen UITableViewCells (for size calculations) not respecting size class?

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

Here is my structure of views for this detail view (blogging application, I want to view the entire post which has dynamic height inside of a scrollview):
UIView
-UIScrollView
-SinglePostView (custom view)
-Title
-Subtitle
-Content
Normally to add a 'single post view' to a UIView I simply instantiate it, feed it my Post object and then add a single constraint that pins the width of the singlePostView to the superview, at which point things get laid out nicely. However when I try to add it to a scroll view, it doesn't show up nor does it scroll.
UIScrollView *scrollView = [[UIScrollView alloc] init];
[self.view addSubview:scrollView];
NOBSinglePostView *singlePost = [[NOBSinglePostView alloc] initWithPost:self.post];
[scrollView addSubview:singlePost];
singlePost.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView,singlePost);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[singlePost]|" options:0 metrics: 0 views:viewsDictionary]];
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.
in auto layout
frame of scrollview is decided by constraints between scrollview and superview of scrollview.
contentSize of scrollview is decided by constraints between scrollview and subview of scrollview.
you should set the size of singlePostView. ScrollView calculate contentSize from it. (you need to add size constraints explicitly)
CGFloat heightOfSinglePostView = calculate..
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[singlePost(heightOfSinglePostView)]|" options:0 metrics: 0 views:viewsDictionary]];

UIView with UILabel subview - responding to AutoLayout changes

I've got a UIView with a UILabel subview that has constraints defined like so:
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[_messageLabel]-50-|" options:0 metrics:nil views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[_messageLabel]-0-|" options:0 metrics:nil views:views];
[self addConstraints:constraints];
In certain instances, the label is not big enough to show all the text, so it is truncated.
When I adjust the size of the UIView within an animation block, the label animates its change in size as appropriate. However, the re-drawing of the text within it kind of 'jumps' -- and fair enough, i wouldn't expect the label to animate a change in the internal drawing of its text.
Anyway, what I'd like to do is fade out this label and perhaps fade in a second to avoid this jerkiness.
My question: Is there a good callback on UIView as to when it will respond to an auto layout change? or is that simply done in layoutSubviews?

Autolayout with overlapping views

I've two views (Text/Image) where one covers the total width of the screen, the other one is an image which sits in lower right corner of the text view.
Up to now I was not able write layout constraints to have both view right and bottom aligned to each other. Here is what I tried to accomplish it but the statusA1 is incorrect.
NSArray *horizontalA = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[answerA]|" options:0 metrics:nil views:viewsDict];
NSArray *statusA1 = [NSLayoutConstraint constraintsWithVisualFormat:#"[answerA][statusA]" options:NSLayoutFormatAlignAllBottom | NSLayoutFormatAlignAllRight metrics:0 views:viewsDict];
Set the vertical spacing between two and remove the bottom contraints on both the subviews.

How to resolve ambiguous layout?

I want an iPad layout that that has two panels side by side, to fill the width of the screen and both are as tall as the screen. My attempts have led to as follows
self.view addConstraints:
#"|[_sidePanel(300)]-1.0-[_mainPanel]|"
#"V:|[_sidePanel]|"
#"V:|[_mainPanel]|"
Inside __sidePanel_ I'm trying to create more constraints on child views.
Note the _sidePanel view is a UIScrollView.
I want to stack 2 views on top of one another in the side panel.
So I add the following constraints to__sidePanel_.
_sidePanelView addConstraints:
#"|[_top(300)]|"
#"|[_bottom(300)]|"
#"V:|[_top]-5.0-[_bottom]|"
It seems I need to specify the width for these two views in order to avoid ambiguity.
But I want the bottom view to fill the remaining space of __sidePanel_.
If I just pin __bottom_ to the bottom of __top_ (which gets a defined height at some point based on its contents) and to the bottom of its parent __sidePanel_, the __sidePanel_ and __bottom_ are both ambiguous; which makes sense i guess since the constraints are awfully similar (and which doesn't get avoided by adding the constraint for __bottom_ to the __sidePanel_ view as opposed to the topmost self.view).
If I hardcode a height for __bottom_, i resolve ambiguity but I don't want a defined height; i want it to fill remaining space in __sidePanel_.
Any suggestions on what I could try to resolve ambiguity but still achieve what I'm after?
You need to specify a height for either top or bottom -- it sounds like top gets a defined height at some point, but you need set a defined height for it initially, which you can change later.
Also, there's no need to specify the widths (300) for either top or bottom, since you've pinned them to the sides of sidePanel, which itself has a defined width. so these constraints worked fine with no ambiguity:
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_sidePanel(300)]-1.0-[_mainPanel]|" options:0 metrics:nil views:viewsDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_sidePanel]|" options:0 metrics:nil views:viewsDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_mainPanel]|" options:0 metrics:nil views:viewsDict]];
[_sidePanelView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_top]|" options:0 metrics:nil views:viewsDict]];
[_sidePanelView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_bottom]|" options:0 metrics:nil views:viewsDict]];
[_sidePanelView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_top]-5.0-[_bottom]|" options:0 metrics:nil views:viewsDict]];
self.topHeightCon = [NSLayoutConstraint constraintWithItem:self.top attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:300];
[self.top addConstraint:self.topHeightCon];
Later, when you calculate the actual height for top, you can use self.topHeightCon.constant = (some value) to adjust its height.
In my case it came down to the fact that the view I was trying to have subviews constrain to its bounds was a UIScrollView, which wasn't happening. I since changed it to a UIView and voila my constraints work. And there you have it.

Resources