Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple UILabels inside a self sizing UITableViewCell

In this iOS 8 app I'm creating, I have a tableview and I need them to be self resizing. I implemented it using Auto Layout and it works. Almost. Here's how it looks now.

enter image description here

There are 3 labels inside a cell. Main label which has the lorem ipsum text. Subtitle which has the string of numbers (Those are two separate labels. Might be confusing because they have the same color.) Then the third label with the small black text.

The first label resized itself correctly with no problem and the second label moves up and down accordingly. But the problem is with the third small label. As you can see, its not resizing itself to fit all the text.

Now there's a weird thing happening. I turn it landscape and here's it is.

enter image description here

Since there is space the label is displaying the entire text its supposed to. Fine. Then I turn it back to portrait.

enter image description here

Now the small label has resized itself to fit all its text but it overflows the cells boundaries. I tried making the cell bigger but it didn't work. Since this is self sizing cells, I don't think that's the correct way even.

I'm not getting any errors or even warning on my auto layout constraints either.

enter image description here

I have set these two lines of code in the viewDidLoad() method.

tableView.estimatedRowHeight = 100 tableView.rowHeight = UITableViewAutomaticDimension 

Can anyone please tell me what I might be doing wrong here?

Since its difficult to answer just by looking at images and I don't have any more code to post beside the above snippet, I uploaded a runnable Xcode project demonstrating the issue here. (There are 2 custom cells. Basically its the same cell just the height is increased in the second one.)

I've been fiddling with auto layout constraints but I can't seem to get this working. Any help would be appreciated.

Thank you.


UPDATE:

With the help of this tutorial I found some helpful pointers. According to it, each subview should have constraints that pin all its sides and there should be constraints that goes from top to bottom which helps auto layout to calculate the height of the cell. In my original post, I had vertical spaces between each label so I think that's the reason auto layout couldn't calculate the proper height.

So I made some changes.

  • I reduced the vertical space between labels to 0 and set the Vertical space constraints between top and middle labels and middle and bottom labels.
  • I added leading, top, trailing constraints to the top label.
  • Leading and trailing to the middle label.
  • Leading, bottom, trailing to the bottom label.

Now here's another weird part. When I first run it, the bottom label cropping issue is still there.

enter image description here

But if I rotate the device to landscape and turn it back to portrait, all the all the cells are resized properly to fit both labels!

enter image description here

Still can't figure out why this doesn't happen at first though. Updated Xcode project is here.

like image 702
Isuru Avatar asked Sep 20 '14 09:09

Isuru


2 Answers

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) 
like image 76
smileyborg Avatar answered Sep 28 '22 06:09

smileyborg


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.

like image 23
fogisland Avatar answered Sep 28 '22 06:09

fogisland