A number of S.O. questions show an autolayout technique to determine the minimum size required by a view requires to fulfil its constraints: [header systemLayoutSizeFittingSize: UILayoutFittingCompressedSize]
Before making the systemLayoutSizeFittingSize: call, all the examples I've seen force a layout update, like this:
[view setNeedsLayout];
[view layoutIfNeeded];
CGFloat height = [view systemLayoutSizeFittingSize: UILayoutFittingCompressedSize].height;
I'd like to know when this is actually necessary as it seems sprinkled in as a ritual seasoning: I'd like to understand why I'm making calls, rather than doing it for luck!
I've just used systemLayoutSizeFittingSize: in some code where I selectively update a view that's a UITableView instance's tableViewHeader (not section header), then resize it. It seems to work fine without the extra calls. I have this in my viewDidLoad:
{
// Remove the view that we don't want.
[self.autoPopulateView removeFromSuperview];
// Resize the table's header view now the unwanted view is removed.
UIView *const header = self.tableView.tableHeaderView;
// Don't explicitly layout.
// [header setNeedsLayout];
// [header layoutIfNeeded];
CGFloat height = [header systemLayoutSizeFittingSize: UILayoutFittingCompressedSize].height;
CGRect frame = header.frame;
frame.size.height = height;
header.frame = frame;
}
Thanks.
There definitely shouldn't be a need to make either of those calls prior to calling systemLayoutSizeFittingSize. As long as all your constraints are in place, you shouldn't need to do anything else.
In fact, forcing a layout pass beforehand is potentially detrimental from a performance standpoint, and I would argue that doing so is not only unnecessary, but actually harmful.
it definately seems like layoutIfNeeded is a catch-all response to seeing inconsistencies of height calculations when using systemLayoutSizeFittingSize - the problem is that the value is very difficult to debug when it's incorrect
In my experience you need to call layoutIfNeeded whenever your target view has updated the constraints in code, for example changing a constant, adding, removing a constraint. Calling setNeedsLayout or setNeedsUpdateConstraints does not work (shrug)
Related
I'm creating my UI entirely in code and using Masonry to constrain the cell's content view's subviews to the appropriate height. I'm using
[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
on iOS 7 for the row height, while iOS 8 handles it automatically.
Everything looks exactly as it should on screen, but in the console I get trainloads of warnings for conflicting constraints, which all seem to be caused by an unasked and unnecessary height constraint on the cell's content view (e.g. <NSLayoutConstraint UITableViewCellContentView.height == 44>).
On iOS 8 I'm setting the table view's rowHeight as UITableViewAutomaticDimension (effectively -1) but still I get this constraint. I'm only adding constraints between the content view and its own subviews, so no constraints between the content view and the cell itself.
Any idea where this constraint comes from and how to make it go away?
Edit: Now I actually found a "solution" of sorts – initially setting the content view's frame to something ridiculous, like CGRectMake(0, 0, 99999, 99999), before adding subviews or constraints, seems to make the warnings go away. But this doesn't exactly smell like the right way to do it, so can anyone tell of a better approach?
I had the same issue and fixed it setting the auto resizing mask of the cell like this:
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.autoresizingMask = .flexibleHeight
}
Also in the controller I set the estimated height and tell the table view to use automatic dimension (in the viewDidLoad method:
self.tableView.estimatedRowHeight = 120
self.tableView.rowHeight = UITableView.automaticDimension
These links helped:
http://useyourloaf.com/blog/2014/08/07/self-sizing-table-view-cells.html
Auto layout constraints issue on iOS7 in UITableViewCell
Hope this helps!
To tack on to the accept answer- after months of trying to get iOS 8's automatic cell sizing to work I discovered an important caveat. The 'estimatedRowHeight' property MUST be set. Either via the tableView directly or by implementing the delegate methods. Even if there's no way to determine a valid estimate, simply providing a value other than the default (0.0) has demonstrably allowed iOS 8's cell layout to work in my testing.
Regarding to the "solution" mentioned in the edit in the question (setting the contentView frame to something big temporarily), here's proof this is a good "solution":
https://github.com/smileyborg/TableViewCellWithAutoLayoutiOS8/blob/master/TableViewCellWithAutoLayout/TableViewController/TableViewCell.swift
// Note: if the constraints you add below require a larger cell size than the current size (which is likely to be the default size {320, 44}), you'll get an exception.
// As a fix, you can temporarily increase the size of the cell's contentView so that this does not occur using code similar to the line below.
// See here for further discussion: https://github.com/Alex311/TableCellWithAutoLayout/commit/bde387b27e33605eeac3465475d2f2ff9775f163#commitcomment-4633188
// contentView.bounds = CGRect(x: 0.0, y: 0.0, width: 99999.0, height: 99999.0)
It's hacky but it seems to work.
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
//self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
self.itemView = [CustomItemView new];
[self.contentView addSubview:self.itemView];
}
return self;
}
set translatesAutoresizingMaskIntoConstraints to NO is not work for me
, but autoresizingMask = UIViewAutoresizingFlexibleHeight is well.
you should also making constraints like this:
- (void)updateConstraints {
[self.itemView mas_updateConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.top.equalTo(0);
//make.bottom.equalTo(0);
make.bottom.lessThanOrEqualTo(0);
}];
[super updateConstraints];
}
bottom constraints not just equalTo contentView's bottom, you should use lessThanOrEqualTo
hope this is work to you!
I found out that in some cases, setting an estimatedHeight that is many times bigger the height of my average cell fixed most if not all warnings and had no negative impact on the display.
i.e.:
Setting self.tableView.estimatedRowHeight = 500.0f while most rows are only about 100.0f in height fixed my issue.
In the code below, these four methods are called for layout reasoning. I'm a little confused why all of them are needed, though, and what they do differently from one another. They're used in the process to make a cell's height be dynamic with Auto Layout. (Taken from this repository from this question.)
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
And it's from this block of code for the cell's height:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
RJTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[cell updateFonts];
NSDictionary *dataSourceItem = [self.model.dataSource objectAtIndex:indexPath.row];
cell.titleLabel.text = [dataSourceItem valueForKey:#"title"];
cell.bodyLabel.text = [dataSourceItem valueForKey:#"body"];
cell.bodyLabel.preferredMaxLayoutWidth = tableView.bounds.size.width - (kLabelHorizontalInsets * 2.0f);
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height;
}
But what are they doing differently? Why are they all needed?
Layout
Suppose you encapsulate your view logic in a UIView subclass, and call it SomeView. This means that SomeView should know how to layout itself, that is, how to position some other views inside it (you can also create a view that draws itself without using any subviews, but that's beyond the needs of an average developer).
This layout is done by [SomeView layoutSubviews]. You have an option of overriding it:
subview.frame = CGRectMake(100 + shiftX, 50 + shiftY, 250 + shiftX - paddingRight, ...
// Oh no, I think I didn't do it right.
but you rarely need to do so. In the dark ages of Cocoa Touch, this manual layout was widespread, but now I'd say 99% of layouts can be covered with Auto Layout.
The system needs to know when it should call [UIView layoutSubviews]. It's obviously done the first time you need to draw a view, but it may be also called whenever the superview frame changes. Here's a detailed explanation.
So the system often calls [view layoutIfNeeded]. You can also call it at any time, but this will have an effect only if there is some event that has called [view setNeedsLayout] or if you have called it manually, as in this case.
Constraints
The Auto Layout (capitalized this way in the documentation) is called like that because you're leaving [SomeView layoutSubviews] as it is inherited from UIView and
describe the position of your subviews instead in terms of constraints.
When using Auto Layout, system will perform calls to [view updateConstraintsIfNeeded] at each layout pass. However, only if the flag [view setNeedsUpdateConstraints]; is set, the method calls into -updateConstraints (which does the real job).
If you don't use Auto Layout, those methods are not relevant.
You can implement it like in this example.
Your example
It's rarely necessary to call -layoutIfNeeded and -updateConstraintsIfNeeded directly, because UI engine will do that automatically at each layout pass. However, in this case the author has chosen to call them immediately; this is because the resulting height is needed right now, not at some point in the future.
This method of updating the cell's height seems right. Note that cell could be a newly created cell and thus not added into view hierarchy yet; this does not impact its ability to layout itself.
Conclusion
In your custom view go with the following options, starting from the most 'universal' to most 'customized':
Create constraints during view creation (manually or in the IB)
If you need to change the constraints later, override -updateConstraints.
If have a complex layout that cannot be described by above means, override -layoutSubviews.
In the code that changes something that could make your view's constraints change, call
[view setNeedsUpdateConstraints];
If you need results immediately, call also
[view updateConstraintsIfNeeded];
If the code changes view's frame use
[view setNeedsLayout];
and finally if you want the results immediately, call
[view layoutIfNeeded];
This is why all four calls are required in this case.
Additional materials
Take a look at detailed explanation in the article Advanced Auto Layout Toolbox, objc.io issue #3
Following question is sort-of continuation of this one:
iOS: Multi-line UILabel in Auto Layout
The main idea is that every view is supposed to state it's "preferred" (intrinsic) size so that AutoLayout can know how to display it properly.
UILabel is just an example of a situation where a view cannot by itself know what size it needs for display. It depends on what width is provided.
As mwhuss pointed out, setPreferredMaxLayoutWidth did the trick of making the label span across multiple lines. But that is not the main question here. The question is where and when do I get this width value that I send as an argument to setPreferredMaxLayoutWidth.
I managed to make something that looks legit, so correct me if I am wrong in any way and tell me please if you know a better way.
In the UIView's
-(CGSize) intrinsicContentSize
I setPreferredMaxLayoutWidth for my UILabels according to self.frame.width.
UIViewController's
-(void) viewDidLayoutSubviews
is the first callback method I know where subviews of the main view are appointed with their exact frames that they inhabit on the screen. From inside that method I, then, operate on my subviews, invalidating their intrinsic sizes so that UILabels are broken into multiple lines based on the width that was appointed to them.
There's an answer this question on objc.io in the "Intrinsic Content Size of Multi-Line Text" section of Advanced Auto Layout Toolbox. Here's the relevant info:
The intrinsic content size of UILabel and NSTextField is ambiguous for multi-line text. The height of the text depends on the width of the lines, which is yet to be determined when solving the constraints. In order to solve this problem, both classes have a new property called preferredMaxLayoutWidth, which specifies the maximum line width for calculating the intrinsic content size.
Since we usually don’t know this value in advance, we need to take a two-step approach to get this right. First we let Auto Layout do its work, and then we use the resulting frame in the layout pass to update the preferred maximum width and trigger layout again.
The code they give for use inside a view controller:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[self.view layoutIfNeeded];
}
Take a look at their post, there's more information about why it's necessary to do the layout twice.
It seems annoying that a UILabel doesn't default to its width for the preferred max layout width, if you've got constraints that are unambiguously defining that width for you.
In nearly every single case I've used labels under Autolayout, the preferred max layout width has been the actual width of the label, once the rest of my layout has been performed.
So, to make this happen automatically, I have used a UILabel subclass, which overrides setBounds:. Here, call the super implementation, then, if it isn't the case already, set the preferred max layout width to be the bounds size width.
The emphasis is important - setting preferred max layout causes another layout pass to be performed, so you can end up with an infinite loop.
Update
My original answer appears to be helpful so I have left it untouched below, however, in my own projects I have found a more reliable solution that works around bugs in iOS 7 and iOS 8.
https://github.com/nicksnyder/ios-cell-layout
Original answer
This is a complete solution that works for me on iOS 7 and iOS 8
Objective C
#implementation AutoLabel
- (void)setBounds:(CGRect)bounds {
if (bounds.size.width != self.bounds.size.width) {
[self setNeedsUpdateConstraints];
}
[super setBounds:bounds];
}
- (void)updateConstraints {
if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
[super updateConstraints];
}
#end
Swift
import Foundation
class EPKAutoLabel: UILabel {
override var bounds: CGRect {
didSet {
if (bounds.size.width != oldValue.size.width) {
self.setNeedsUpdateConstraints();
}
}
}
override func updateConstraints() {
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
super.updateConstraints()
}
}
We had a situation where an auto-layouted UILabel inside a UIScrollView laid out fine in portrait, but when rotated to landscape the height of the UILabel wasn't recalculated.
We found that the answer from #jrturton fixed this, presumably because now the preferredMaxLayoutWidth is correctly set.
Here's the code we used. Just set the Custom class from Interface builder to be CVFixedWidthMultiLineLabel.
CVFixedWidthMultiLineLabel.h
#interface CVFixedWidthMultiLineLabel : UILabel
#end
CVFixedWidthMultiLineLabel.m
#implementation CVFixedWidthMultiLineLabel
// Fix for layout failure for multi-line text from
// http://stackoverflow.com/questions/17491376/ios-autolayout-multi-line-uilabel
- (void) setBounds:(CGRect)bounds {
[super setBounds:bounds];
if (bounds.size.width != self.preferredMaxLayoutWidth) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
}
#end
Using boundingRectWithSize
I resolved my struggle with two multi-line labels in a legacy UITableViewCell that was using "\n" as a line-break by measuring the desired width like this:
- (CGFloat)preferredMaxLayoutWidthForLabel:(UILabel *)label
{
CGFloat preferredMaxLayoutWidth = 0.0f;
NSString *text = label.text;
UIFont *font = label.font;
if (font != nil) {
NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = #{NSFontAttributeName: font,
NSParagraphStyleAttributeName: [mutableParagraphStyle copy]};
CGRect boundingRect = [text boundingRectWithSize:CGSizeZero options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
preferredMaxLayoutWidth = ceilf(boundingRect.size.width);
NSLog(#"Preferred max layout width for %# is %0.0f", text, preferredMaxLayoutWidth);
}
return preferredMaxLayoutWidth;
}
Then calling the method was then as simple as:
CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];
As I'm not allowed to add a comment, I'm obliged to add it as an answer.
The version of jrturton only worked for me if I call layoutIfNeeded in updateViewConstraints before getting the preferredMaxLayoutWidth of the label in question.
Without the call to layoutIfNeeded the preferredMaxLayoutWidth was always 0 in updateViewConstraints. And yet, it had always the desired value when checked in setBounds:. I didn't manage to get to know WHEN the correct preferredMaxLayoutWidth was set. I override setPreferredMaxLayoutWidth: on the UILabel subclass, but it never got called.
Summarized, I:
...sublcassed UILabel
...and override setBounds: to, if not already set, set preferredMaxLayoutWidth to CGRectGetWidth(bounds)
...call [super updateViewConstraints] before the following
...call layoutIfNeeded before getting preferredMaxLayoutWidth to be used in label's size calculation
EDIT: This workaround only seems to work, or be needed, sometimes. I just had an issue (iOS 7/8) where the label's height were not correctly calculated, as preferredMaxLayoutWidth returned 0 after the layout process had been executed once. So after some trial and error (and having found this Blog entry) I switched to using UILabel again and just set top, bottom, left and right auto layout constraints. And for whatever reason the label's height was set correctly after updating the text.
As suggested by another answer I tried to override viewDidLayoutSubviews:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
_subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
[self.view layoutIfNeeded];
}
This worked, but it was visible on the UI and caused a "visible flicker" i.e. first the label was rendered with the height of two lines, then it was re-rendered with the height of only one line.
This was not acceptable for me.
I found then a better solution by overriding updateViewConstraints:
-(void)updateViewConstraints {
[super updateViewConstraints];
// Multiline-Labels and Autolayout do not work well together:
// In landscape mode the width is still "portrait" when the label determines the count of lines
// Therefore the preferredMaxLayoutWidth must be set
_subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
}
This was the better solution for me, because it did not cause the "visual flickering".
A clean solution is to set rowcount = 0 and to use a property for the heightconstraint of your label. Then after the content is set call
CGSize sizeThatFitsLabel = [_subtitleLabel sizeThatFits:CGSizeMake(_subtitleLabel.frame.size.width, MAXFLOAT)];
_subtitleLabelHeightConstraint.constant = ceilf(sizeThatFitsLabel.height);
-(void) updateViewConstraints has a problem since iOS 7.1.
In iOS 8, you can fix multi-line label layout problems in a cell by calling cell.layoutIfNeeded() after dequeuing and configuring the cell. The call is harmless in iOS 9.
See Nick Snyder's answer. This solution was taken from his code at https://github.com/nicksnyder/ios-cell-layout/blob/master/CellLayout/TableViewController.swift.
I've insert an UITextView in a view with Interface builder, now I would like to change its frame size so it fits the content programmatically. The problem is that the size seems locked and unchangable from code because of the constraints. If I disable in file inspector use auto-layout every object gets the constraints removed, but I only want to change the UITextView not the other objects.
[textview setFrame:CGRectMake(x,y,w,h)]; // This doesn't do anything to the uitextview
If you're using constraints, then you must use constraints to change the UITextView's size. Set up outlets in the nib to get access from your code to the constraints you need to change. You can set a constraint's constant in real time, and this is usually sufficient.
Just to clarify the reasons for this: you cannot change a view's frame if it is being positioned by constraints. Well, you can, but it's fruitless and it's bad practice. This is because the constraints themselves will be used by the layout system to change the view's frame for you! Thus, you can change the frame, but the layout system will then read and resolve the constraints and change the frame back again.
Think of constraints as a "to-do list" for the layout system. Constraints do nothing in and of themselves; they are just a list of rules. It is the layout system that does the work (when layoutSubviews is called). Every time layout is needed, the layout system comes along, reads the constraints, works out how to obey them, and does so - by setting the frames of your views. You need to work with that system, not against it.
All of UIViews and classes are inherited from UIView have same property is: "Lock" in XIB file.
If you want to change frame of those views. You must set Lock is "Nothing"
If you don't care to become a constraint expert, do it like this (in your view controller.)
#property UITextView *textView;
//Create UITextView on the fly in viewWillAppear
rect = CGRectMake(194., 180., 464., 524.);
_textView = [[UITextView alloc] initWithFrame:rect];
[self.view addSubview:_textView];
//Now you can resize it at will
CGRect oldFrame = _textView.frame;
CGRect newFrame = oldFrame;
newFrame.size.height += 300;
[_textView setFrame:newFrame];
I need to get the full height of a UITableView (i.e. the height at which there would be nothing more to scroll). Is there any way to do this?
I've tried [tableView sizeThatFits:CGSizeZero], but that only returns a 0x0 CGSize.
Try the contentSize method, which is inherited from UITableView’s superclass, UIScrollView. However, you may find that contentSize returns an incorrect or out of date value, so you should probably call layoutIfNeeded first to recalculate the table’s layout.
- (CGFloat)tableViewHeight
{
[tableView layoutIfNeeded];
return [tableView contentSize].height;
}
Obligatory Swift 3.0 & 2.2 answer.
var tableViewHeight: CGFloat {
tableView.layoutIfNeeded()
return tableView.contentSize.height
}
Try passing in a different CGSize parameter instead of CGSizeZero. The sizeThatFits: method uses that parameter to calculate its result. Try passing in self.view.size from whatever class is making that call.
If a table view rows count changed and you indeed need to know the content size of table view incorporating the last changes, I didn't find that layoutIfNeeded method actually helps.
After a little bit hacking, I get to know how to force table view recalculate its content size. In my case, it is enough to reset table view frame to get it working:
- (CGSize)com_lohika_contentSize
{
CGRect theFrame = self.frame;
self.frame = CGRectZero;
self.frame = theFrame;
[self layoutIfNeeded];
return [self contentSize];
}