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.
Related
For certain cases with AutoLayout I need to know the width of my view (most nested subview) within it's superview. With AutoLayout in iOS 8 I was able to rely on layoutIfNeeded for the layout system to layout the frames and get the proper width before I do this calculation.
An example would be something like this:
- (CGSize)intrinsicContentSize {
[self layoutIfNeeded];
CGSize size = [self roundedSizeAccountingLeftRightInsets:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX)];
size.height += self.insets.top + self.insets.bottom;
return size;
}
This no longer works with iOS 9. I'm sure that all constraints to be able to calculate the width are set (usually just leading, trailing constraints bound to the superview).
I noticed this in the release notes for iOS 9 but I wasn't really able to interpret it.
In iOS 9, when layoutIfNeeded is sent to a view and all of the following conditions are satisfied (which is not common), we apply fitting-size constraints (width/height = 0 at UILayoutPriorityFittingSizeLevel) instead of required size constraints (width/height required to match current size):
The receiver is not yet in the subtree of a view that hosts a layout engine, such as window, view controller view (unless you have set translatesAutoresizingMaskIntoConstraints to NO on that view—or created constraints that have one item in its subtree and one item outside it), table view cell content view, and so on.
The final ancestor (that is, top-level view) of the receiver has translatesAutoresizingMaskIntoConstraints set to NO.
The top-level view has a subview that is not a UIViewController-owned layout guide that also has translatesAutoresizingMaskIntoConstraints set to NO.
Under condition 1, we create a temporary layout engine from the top-level view and add all the constraints from the subtree to it. The problem is that we need to add some constraints that make the size of the top-level view unambiguous in the layout engine. The old behavior (prior to iOS 9) was that we would add constraints to restrict the size of the top-level view to its current bounds for any situation under condition 1. This really doesn’t make sense when you add conditions 2 and 3 and can result in unsatisfiable-constraints logging and broken layout.
So in iOS 9, for this special case only, we use fitting-size constraints instead.
This means that if you are sending layoutIfNeeded to a view under these conditions in iOS 9, you must be sure that either you have sufficient constraints to establish a size for the top-level view (which usually, though not always, is the receiver) or you must add temporary size constraints to the top-level view of layout size you desire before sending layoutIfNeeded, and remove them afterward.
Has anyone else encountered this issue, or familiar with how to solve?
Edit: Couple More Examples
I usually do this when I need to know explicitly what the layout width will be of the superview because constraints of the subview are dependent on this value and can't be expressed with preferredMaxLayoutWidth.
The first example is a custom view with an array of labels. When constraints are updated I need to know the width so I can know if those labels will continue on the same line or move down to the next line.
- (void)updateConstraints {
[self layoutIfNeeded];
CGFloat width = self.view.bounds.size.width;
for (UILabel *label in self.labels) {
CGSize labelSize = [label sizeThatFits:CGSizeZero];
CGFloat minLabelWidth = MAX(12, labelSize.width);
labelSize.width = minLabelWidth;
lineWidth += labelSize.width + 10;
if (lineWidth >= width) {
// update some variables to where I will actually be applying constraints
}
[label mas_updateConstraints:^(MASConstraintMaker *make) {
// constraint magic
}];
[super updateConstraints];
}
One more:
In this example there will sometimes be a text label that is shown based on a condition. If it needs to be shown I expand it to it's appropriate height constrained to the width of it's superview (it only has insets to it's leading and trailing superview). If it doesn't need to be shown I collapse the label.
- (void)updateConstraints {
// Need layout pass to get the proper width.
[self layoutIfNeeded];
CGFloat textHeight = [self.label sizeThatFits:CGSizeMake(self.bounds.size.width - 32, CGFLOAT_MAX)].height;
[self.label mas_remakeConstraints:^(MASConstraintMaker *make) {
// update other constraints
make.height.equalTo( showThisText ? #(textHeight) : #0 );
}];
[super updateConstraints];
}
There can also be a case when I need a textField to be shown and not be pushed off the screen by other elements along the x axis so I have to give it a fixed width via constraints but I need to know the max width before I do that
- (void)updateConstraints {
[self layoutIfNeeded];
CGFloat textFieldWidth = self.bounds.size.width - someVariable;
[self.textField mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(#(textFieldWidth));
}];
[super updateConstraints];
}
I ended up overriding layoutSubviews since this is a UIView subclass and it seems to be working now with this code
- (void)layoutSubviews {
[super layoutSubviews];
static CGSize viewBounds = { 0, 0 };
static CGSize previousViewBounds = { 0, 0 };
[self setNeedsUpdateConstraints];
viewBounds = CGSizeMake(self.bounds.size.width, self.bounds.size.height);
if (!CGSizeEqualToSize(viewBounds, previousViewBounds)) [self setNeedsUpdateConstraints];
previousViewBounds = viewBounds;
}
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.
I am using UILabels with numberOfLines=0 and lineBreakMode = NSLineBreakByWordWrapping in a "dynamic" UITableViewCell . I utilize AutoLayout to attach the label to its container cell at the top, bottom, trailing and leading edges. In iOS7, the label's height updates dynamically with its content, however in iOS8 it truncates prematurely (usually at 1 line, but sometimes at 2). In iOS8, I have confirmed that the intrinsicContentSize of the label is dynamically updating as expected, but the label's frame is not following suit. Here is the difference seen between iOS7 and iOS8:
I have read of similar bugs in iOS8, but have been unable to implement a workaround. I have tried setting the label's text to #"" and back to #"Abraham Lincoln: Vampire Hunter" (as mentioned in this question), but that does not remedy the issue. Also notice that in iOS8, the cell size does increase according to the label's intrinsicContentSize, but for whatever reason, the height of the label itself does not. Any suggestions would be greatly appreciated. I would be glad to provide more information as well.
iOS 8
iOS 7
This is my walk around
subclass of UILabel, and override this function.
-(CGRect)textRectForBounds:(CGRect)bounds
limitedToNumberOfLines:(NSInteger)numberOfLines
{
CGRect rect = [super textRectForBounds:bounds
limitedToNumberOfLines:numberOfLines];
float height = rect.size.height;
int hightRound = (int)roundf(height);
rect.size.height = hightRound;
return rect;
}
I have a UITableView with cells that have a fixed height of 100 points. The cells are created in a xib file that uses 3 constraints to pin a UILabel to the left, right and top edges of the cell's contentView. The label's vertical hugging priority is set to 1000 because I want the cell's height to be as small as possible.
When the width of the cell in the xib file is set to 320 points, the same as the tableView's width on the iPhone, autolayout works as expected. However, when I set the width of the cell to less than 320 points, I get unexpected results. (I want to use the same cell in tableViews that have different widths, e.g. in a universal app)
For example: when I set the width to 224 points and give the label a text that takes up 2 lines at that width, the label's height will increase to fit the 2 lines, but when the cell is then resized to 320 points to fit in a tableView of that width, the text only takes up 1 line, but the height of the label remains at 2 lines.
I have put a sample project on GitHub to demonstrate the problem: https://github.com/bluecrowbar/CellLayout
Is there a way to make the UILabel always resize to hug its text content?
Adding this in the cell subclass works:
- (void)layoutSubviews
{
[super layoutSubviews];
[self.contentView layoutIfNeeded];
self.myLabel.preferredMaxLayoutWidth = self.myLabel.frame.size.width;
}
I found this on http://useyourloaf.com/blog/2014/02/14/table-view-cells-with-varying-row-heights.html.
Update 1: This answer was for iOS 7. I find auto layout in table view cells to be very unreliable since iOS 8, even for very simple layouts. After lots of experimentation, I (mostly) went back to doing manual layout and manual calculation of the cell's height.
Update 2: I've run some tests on iOS 9 and it seems that UITableViewAutomaticDimension finally works as advertised. Yay!
Stupid bug! I've lost almost one day in this problem and finally I solved It with Steven Vandewghe's solution.
Swift version:
override func layoutSubviews() {
super.layoutSubviews()
self.contentView.layoutIfNeeded()
self.myLabel.preferredMaxLayoutWidth = self.myLabel.frame.size.width
}
Since you're constraining the label's width, the intrinsicContentSize honors that width and adjusts the height. And this sets up a chicken and egg problem:
The cell's Auto Layout result depends on the label's intrinsicContentSize
The label's intrinsicContentSize depends on the label's width
The label's width depends on the cell's Auto Layout result
So what happens is that the cell's layout is only calculated once in which (2) is based on the static width in the XIB file and this results in the wrong label height.
You can solve this by iterating. That is, repeat the Auto Layout calculation after the label's width has been set by the first calculation. Something like this in your custom cell will work:
- (void)drawRect:(CGRect)rect
{
CGSize size = self.myLabel.bounds.size;
// tell the label to size itself based on the current width
[self.myLabel sizeToFit];
if (!CGSizeEqualToSize(size, self.myLabel.bounds.size)) {
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
}
[super drawRect:rect];
}
original solution does not work reliably:
- (void)layoutSubviews
{
[super layoutSubviews];
// check for need to re-evaluate constraints on next run loop
// cycle after the layout has been finalized
dispatch_async(dispatch_get_main_queue(), ^{
CGSize size = self.myLabel.bounds.size;
// tell the label to size itself based on the current width
[self.myLabel sizeToFit];
if (!CGSizeEqualToSize(size, self.myLabel.bounds.size)) {
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
}
});
}
I'm using XCode 10 with iOS 12 and I still get autolayout problems with cells not being given the correct height when the table is first presented. Timothy Moose's answer didn't fix the problem for me, but based on his explanation I came up with a solution which does work for me.
I subclass UITableViewController and override the viewDidLayoutSubviews message to check for width changes, and then force a table update if the width does change. This fixes the problem before the view is presented, which makes it look much nicer than my other efforts.
First add a property to your custom UITableViewController subclass to track the previous width:
#property (nonatomic) CGFloat previousWidth;
Then override viewDidLayoutSubviews to check for width changes:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat width = self.view.frame.size.width;
if (self.previousWidth != width) {
self.previousWidth = width;
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
}
This fixed issues with my table cells sometimes being given the wrong height initially.
I know this is an old issue, but maybe this UILabel subclass can also help for some:
class AutoSizeLabel: 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()
}
}
Note: works also for cases when your UILabel won't size itself correctly when inside of a StackView
I usually add these two lines to viewDidLoad()
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 96
This will automatically resize the cell
I have a uilabel setup in a view. It doesn't have a width constraint, but its width is instead determined by a leading constraint to the thumbnail image, and a trailing constraint to the edge of the view.
The label is set to have 0 lines, and to word wrap. My understanding is that this should cause the frame of the uilabel to grow, and indeed it does sometimes. (Previous to auto layout, I would calculate and update the frame of the label in code).
So the result is, it works correctly in some instance and not others. See most cells working correctly there, but the last cell appears to be too big. In fact it's the right size. The title "Fair Oaks Smog Check Test" actually ends with "Only". So my calcuation for the cell size is right, it should be that size. However the label doesn't wrap the text for whatever reason. It's frame width does not extend off to the right, so that's not the issue.
So what is going on here? It's 100% consistent, always on that cell and not the ones above it, which makes me think it's related to the size of the text, and UILabel isn't re-laying out the text once this view is added to the cell (which makes it actually smaller width wise).
Any thoughts?
Some additional information
The height of the cells is calculated from one sample cell I create and store in a static variable:
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.items.count == 0) {
return 60;
}
static TCAnswerBuilderCell *cell = nil;
static dispatch_once_t pred;
dispatch_once(&pred,
^{
// get a sample cellonce
cell = [tableView dequeueReusableCellWithIdentifier:TC_ANSWER_BUILDER_CELL];
});
[cell configureCellWithThirdPartyObject:self.items[indexPath.row]];
return [cell heightForCellWithTableWidth:self.tableView.frame.size.width];
}
I configure the cell with my data object on the fly, and then call a method I have on it which calculates the height of the cell with a given table width (can't always rely on the cell frame being correct initially).
This in turn calls a height method on my view, since it is really where the label lives:
- (CGFloat)heightForCellWithTableWidth:(CGFloat)tableWidth {
// subtract 38 from the constraint above
return [self.thirdPartyAnswerView heightForViewWithViewWidth:tableWidth - 38];
}
This method determines the height by figuring out the correct width of the label, and then doing a calculation:
- (CGFloat)heightForViewWithViewWidth:(CGFloat)viewWidth {
CGFloat widthForCalc = viewWidth - self.imageFrameLeadingSpaceConstraint.constant - self.thumbnailFrameWidthConstraint.constant - self.titleLabelLeadingSpaceConstraint.constant;
CGSize size = [self.titleLabel.text sizeWithFont:self.titleLabel.font constrainedToSize:CGSizeMake(widthForCalc, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
CGFloat returnHeight = self.frame.size.height - self.titleLabel.frame.size.height + size.height;
CGFloat height = returnHeight < self.frame.size.height ? self.frame.size.height : returnHeight;
return height;
}
This works 100% correctly.
The cells are created obviously in cellForRowAtIndexPath and immediately configured:
if (self.items.count > 0) {
TCAnswerBuilderCell *cell = [tableView dequeueReusableCellWithIdentifier:TC_ANSWER_BUILDER_CELL forIndexPath:indexPath];
[cell configureCellWithThirdPartyObject:self.items[indexPath.row]];
return cell;
}
In configuration of the cell, my view is loaded from a nib (it's re-used elsewhere, which is why it's not directly in the cell). The cell adds it as follows:
- (void) configureCellWithThirdPartyObject:(TCThirdPartyObject *)object {
self.detailDisclosureImageView.hidden = NO;
if (!self.thirdPartyAnswerView) {
self.thirdPartyAnswerView = [TCThirdPartyAPIHelper thirdPartyAnswerViewForThirdPartyAPIServiceType:object.thirdPartyAPIType];
self.thirdPartyAnswerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.thirdPartyAnswerView];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_thirdPartyAnswerView]-38-|" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_thirdPartyAnswerView)]];
}
[self.thirdPartyAnswerView configureViewForThirdPartyObject:object forViewStyle:TCThirdPartyAnswerViewStyleSearchCell];
}
Finally my view configuration looks like this:
- (void) configureViewForThirdPartyObject:(TCTPOPlace *)object forViewStyle:(TCThirdPartyAnswerViewStyle) style {
self.titleLabel.text = object.name;
self.addressLabel.text = [NSString stringWithFormat:#"%#, %#, %#", object.address, object.city, object.state];
self.ratingsLabel.text = [NSString stringWithFormat:#"%d Ratings", object.reviewCount];
NSString *ratingImageName = [NSString stringWithFormat:#"yelp_star_rating_%.1f.png", object.rating];
UIImage *ratingsImage = [UIImage imageNamed:ratingImageName];
if (ratingsImage) {
self.ratingImageView.image = ratingsImage;
}
if (object.imageUrl) {
[self.thumbnailImageView setImageWithURL:[NSURL URLWithString:object.imageUrl] completed:nil];
}
}
A sort of solution, but I don't understand why
My subview was designed at 320 width, but has no constraints of its own for width
The subview was added to the cell, but with horizontal constraints that look like this:
#"|[_thirdPartyAnswerView]-38-|"
The view was configured immediately after being added to the cell, meaning the text for the titleLabel was set right then.
For whatever reason, the text was laid out as if the view had the full 320 instead of 282.
The label was never updated, even though the frame of the subview was updated to 282, and there were constraints on the label that would keep it sized correctly.
Changing the size of the view in the xib to be 282 fixed the issue, because the label has the right size to begin with.
I'm still not understanding why the label doesn't re-lay out after the size of the parent view is updated when it has both leading and trailing constraints.
SOLVED
See Matt's answer below: https://stackoverflow.com/a/15514707/287403
In case you don't read the comment, the primary problem was that I was unknowingly setting preferredMaxLayoutWidth via IB when designing a view at a bigger width than it would be shown (in some cases). preferredMaxLayoutWidth is what is used to determine where the text wraps. So even though my view and titleLabel correctly resized, the preferredMaxLayoutWidth was still at the old value, and causing wrapping at unexpected points. Setting the titleLabel instead to it's automatic size (⌘= in IB), and updating the preferredMaxLayoutWidth dynamically in layoutSubviews before calling super was the key. Thanks Matt!
I'm someone who has written an app that uses autolayout of five labels in a cell in a table whose cells have different heights, where the labels resize themselves according to what's in them, and it does work. I'm going to suggest, therefore, that the reason you're having trouble might be that your constraints are under-determining the layout - that is, that you've got ambiguous layout for the elements of the cell. I can't test that hypothesis because I can't see your constraints. But you can easily check (I think) by using po [[UIWindow keyWindow] _autolayoutTrace] when paused in the debugger.
Also I have one other suggestion (sorry to just throw stuff at you): make sure you're setting the label's preferredMaxLayoutWidth. This is crucial because it's the width at which the label will stop growing horizontally and start growing vertically.
I had the same problem and solved it using a suggestion from this answer. In a subclass of UILabel I placed this code:
- (void)layoutSubviews
{
[super layoutSubviews];
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
I don't understand why this is not the default behavior of UILabel, or at least why you cannot just enable this behavior via a flag.
I am a little concerned that preferredMaxLayoutWidth is being set in the middle of the layout process, but I see no easy way around that.
Also, check that you are passing integral numbers to your layout constraints in code.
For me it happened that after some calculations (e.g. convertPoint:toView:), I was passing in something like 23.99999997, and eventually this lead to a 2-line label displaying as a one-liner (although its frame seemed to be calculated correctly). In my case CGRectIntegral did the trick!
Rounding errors could kill ya :)