Multiple UILabels inside a self sizing UITableViewCell
I met the same issue as you and I found a simple solution to resolve it.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// dequeue cell...
// do autolayout staffs...or if the autolayout rule has been set in xib, do nothing
[cell layoutIfNeeded];
return cell;
}
And the self-sizing worked well. In my code, I laid two labels in vertical, both of them are dynamic height. The height of cell is correctly set to contain the two labels.
Assuming you don't have any errors with your constraints as others have suggested, this problem seems to stem from using a UILabel that allows multiple lines in conjunction with a UITableViewCellAccessory. When iOS lays out the cell and determines the height, it does not account for the offset change in width that occurs because of this accessory, and you get truncation where you wouldn't expect to.
Assuming you want the UILabel to extend the full width of the content view, I wrote up a method that fixes this for all font sizes
-(void)fixWidth:(UILabel *)label forCell:(UITableViewCell *)cell {
float offset = 0;
switch ([cell accessoryType]) {
case UITableViewCellAccessoryCheckmark:
offset = 39.0;
break;
case UITableViewCellAccessoryDetailButton:
offset = 47.0;
break;
case UITableViewCellAccessoryDetailDisclosureButton:
offset = 67.0;
break;
case UITableViewCellAccessoryDisclosureIndicator:
offset = 33.0;
break;
case UITableViewCellAccessoryNone:
offset = 0;
break;
}
[label setPreferredMaxLayoutWidth:CGRectGetWidth([[self tableView]frame]) - offset - 8];
}
Simply put this in your cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
#Setup the cell
...
// Fix layout with accessory view
[self fixWidth:[cell label] forCell:cell];
return cell;
}
Do this for any labels that are going to have multiple lines to adjust the width properly and then recalculate the appropriate heights. This works with dynamic font sizes as well.
Like smileyborg had mentioned, if you weren't using the full width of the contentView you could reference the constraints and subtract them from the width as well.
Edit: I previously was running 'layoutIfNeeded' on the cell but this was creating performance issues and didn't seem to be needed anyway. Removing it hasn't caused any problems for me.
You have two problems here.
1/ Right now, using Visual Format Language, your cell's vertical constraints can be translated like this:
Cell: "V:|-(10)-[nameLabel]-(67)-|"
Then, you set a second group of constraints:
Cell: "V:|-(10)-[nameLabel]-(8)-[pnrLabel]-(2)-[actionsLabel]"
Those two groups of constraints can't mix well and will reveal their ambiguity with your second problem.
2/ For some reasons, actionsLabel
is limited to one line when you launch your app. Then, when you rotate your device to landscape mode, actionsLabel
accepts to be displayed with two lines or more. Then, when you rotate your device back to portrait mode, actionsLabel
keeps displaying two lines or more. But, because actionsLabel
is not really part of your cell's height constraints, it overlap your cell's boundaries.
If you want to solve all those problems, I recommend first that you rebuild your xib file from scratch. This will cure your actionsLabel
strange behavior (two lines or more only when you rotate your device).
Then, you will have to define your constraints like this:
Cell: "V:|-(10)-[nameLabel(>=21)]-(8)-[pnrLabel(>=21)]-(2)-[actionsLabel(>=21)]-(10)-|"
Of course, you can define other minimum height constraints for you labels than (>=21)
. In the same way, your bottom margin can be set to another value than -(10)-
.
Addendum
In order to answer your question, I created a simple project with the previous constraints pattern in my .xib file. The following image may help you build your own constraints.
The issue here is with the multi-line labels' preferredMaxLayoutWidth
property. This is the property that tells the label when it should word wrap. It must be set correctly in order for each label's intrinsicContentSize
to have the correct height, which is ultimately what Auto Layout will be using to determine the cell's height.
Xcode 6 Interface Builder introduced a new option to have this property set to Automatic. Unfortunately, there are some serious bugs (as of Xcode 6.2/iOS 8.2) where this is not set correctly/automatically when loading a cell from a nib or Storyboard.
In order to work around this bug, we need to have the preferredMaxLayoutWidth
set to be exactly equal to the final width of the label once it is displayed in the table view. Effectively, we want to do the following before returning the cell from tableView:cellForRowAtIndexPath:
:
cell.nameLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.nameLabel.frame)
cell.idLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.idLabel.frame)
cell.actionsLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.actionsLabel.frame)
The reason that just adding this code alone doesn't work is because when these 3 lines of code execute in tableView:cellForRowAtIndexPath:
, we are using the width of each label to set the preferredMaxLayoutWidth
-- however, if you check the width of the labels at this point in time, the label width is totally different from what it will end up being once the cell is displayed and its subviews have been laid out.
How do we get the label widths to be accurate at this point, so that they reflect their final width? Here's the code that makes it all come together:
// Inside of tableView:cellForRowAtIndexPath:, after dequeueing the cell
cell.bounds = CGRect(x: 0, y: 0, width: CGRectGetWidth(tableView.bounds), height: 99999)
cell.contentView.bounds = cell.bounds
cell.layoutIfNeeded()
cell.nameLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.nameLabel.frame)
cell.idLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.idLabel.frame)
cell.actionsLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.actionsLabel.frame)
OK, so what are we doing here? Well, you'll notice there are 3 new lines of code added. First, we need to set this table view cell's width so that it matches the actual width of the table view (this assumes the table view has already been laid out and has its final width, which should be the case). We're effectively just making the cell width correct early, since the table view is going to do this eventually.
You'll also notice that we're using 99999
for the height. What's that about? That is a simple workaround for the problem discussed in detail here, where if your constraints require more vertical space than the current height of the cell's contentView, you get a constraint exception that doesn't actually indicate any real problem. The height of the cell or any of its subviews doesn't actually matter at this point, because we only care about getting the final widths for each label.
Next, we make sure that the contentView of the cell has the same size as we just assigned to the cell itself, by setting the contentView's bounds to equal the cell's bounds. This is necessary because all of the auto layout constraints you have created are relative to the contentView, so the contentView must be the correct size in order for them to get solved correctly. Just setting the cell's size manually does not automatically size the contentView to match.
Finally, we force a layout pass on the cell, which will have the auto layout engine solve your constraints and update the frames of all the subviews. Since the cell & contentView now have the same widths they will at runtime in the table view, the label widths will also be correct, which means that the preferredMaxLayoutWidth
set to each label will be accurate and will cause the label to wrap at the right time, which of course means the labels' heights will be set correctly when the cell is used in the table view!
This is definitely an Apple bug in UIKit that we have to workaround for now (so please do file bug reports with Apple so they prioritize a fix!).
One final note: this workaround will run into trouble if your table view cell's contentView
width doesn't extend the full width of the table view, for example when there is a section index showing on the right. In this case, you'll need to make sure that you manually take this into account when setting the width of the cell -- you may need to hardcode these values, something like:
let cellWidth = CGRectGetWidth(tableView.bounds) - kTableViewSectionIndexWidth
cell.bounds = CGRect(x: 0, y: 0, width: cellWidth, height: 99999)