Multiple UILabels inside a self sizing UITableViewCell
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)
Horizontal UILabels inside a self sizing UITableViewCell
There is a way to achieve what you want to do just using constraints. The trick is to set the relation of the constraints between the two lower labels and the bottom to Greater Than or Equal
instead of equal
.
Here is how I set the constraints:
Which has this result:
Or you could set the constraints between the labels top and bottom to Greater Than or Equal
and leave the bottom constraint at equal
:
To get this result:
Multiple UILabels in UITAbleViewCell with dynamic height
Add a (lower priority) fixed height constraint for the cell (e.g. = 40).
Add a top and bottom constraint for each label. Make the top constraint a fixed margin (e.g. = 8). Make the bottom constraint a (higher priority) greater than or equal constraint (e.g. >= 8).
The cell's height will be determined by the taller label, and all the other vertical constraints will still be met.
Update:
It turns out that you don't need a fixed height constraint for the cell, or even need to tweak the horizontal content hugging or compression resistance priorities.
I pinned the labels' top and left or right edge to the superview.margin, = 0, and added a horizontal space between the labels >= 4.
I pinned the labels' bottom edge to the superview.margin, >= 0. I also set both labels' vertical compression resistance priority to 1000.
At this point, we've set the minimum necessary constraints. However, if both labels are long, one label will take most of the width, forcing the other label to be extremely narrow. I arbitrarily added a width >= 100 constraint to both labels to balance things out.
Enable self-sizing in -viewDidLoad
, then assign your labels' text, and have the cell lay itself out. It wasn't necessary to override anything in the custom UITableViewCell
.
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
cell.leftLabel.text = ...;
cell.rightLabel.text = ...;
[cell setNeedsLayout];
[cell layoutIfNeeded];
return cell;
}
UITableViewCell expanding animation with self-sizing labels, visual animation problem
You can almost fix your issues with very few changes.
Two important things:
- For all labels, set both Hugging and Compression Resistance to
1000
(Required) - Make sure you have a complete vertical constraint chain.
Couple drawbacks to your method of setting / clearing the text of the label though. The expand animation will work well, but the collapse animation will look as it does in your original gif --- the text disappears instead of being covered. Also, you have extra spacing at the bottom.
To get around that, you can set a constraint from the bottom of the "description" label - as you likely have it now - to the bottom of the content view... this will be the "expanded" constraint, AND add another constraint from the bottom of the "center 0" label to the bottom of the content view... this will be the "collapsed" constraint.
Those constraints will conflict at first... so change the Priority of the one of those constraints (doesn't matter which) to 750
(Default High) and the other constraint to 250
(Default Low).
When your app is running, you would swap the priorities based on whether you want the cell expanded or collapsed.
I put together a sample app using your layout - You can download it here: https://github.com/DonMag/Maverick
I use background colors to make it easy to see the frames. There is a line in cellForRowAt
that can be un-commented to clear the colors.
The result using your set / clear text approach:
and using the Constraints method:
Is it possible to use self sizing cells within self sizing cells?
The problem with automatically sizing cells inside automatically sized cells is a bit tricky because you have to understand how it works. UITableView
works with estimates most of the time. It usually does not calculate the contentSize
precisely because to calculate it precisely, it has to first instantiate every cell, layout it and then calculate its size.
The precise values are calculated only for cells that are displayed (visible in current scroll frame).
The tricky part is that the inner cells (inside your outer cell) are not displayed until the outer cell is displayed therefore the outer cell does not have size calculated correctly. Also note that UITableView
does not automatically update cell heights unless explicitly said to do so.
The solution, if you really have to do this, is to calculate the height of the outer cell correctly before it is displayed and manually set a height constraint.
If you know the height (from data source), it's easy. If you actually need to calculate the height of the inner table, you can do something like this:
// make the table high enough to display all cells
innerTableHeightConstraint.constant = 2000
// reload table
innerTable.reloadData()
// force layout
innerTable.layoutIfNeeded()
// now the contentSize is correctly calculated
innerTableHeightConstraint.constant = innerTable.contentSize.height
The whole concept is tricky and ideally you should prefer using UICollectionView
or table sections. When you are using inner table views, there won't be any cell reuse for the inner tables and your performance will suffer.
iOS 7/8 UITableView Cell: Two UILabels with dynamic height with auto layout for variable row height
I got it working finally. The solution was for me to explicitly set the Preferred Width to the current frame width.
So basically checking the Explicit check mark in the Label > Preferred Width in the Size Inspector.
ref: http://www.raywenderlich.com/73602/dynamic-table-view-cell-height-auto-layout
download the sample code and see the storyboard setup.
Related Topics
How to Open Mail App from Swift
How to Put an Image in a Realm Database
How to Change Wkwebview or Uiwebview Default Font
How to Programmatically Enable Guided Access (Kiosk Mode) on an Iphone
Table View Images Never Being Released
How to Remove Provisioning Profiles from Xcode
Implement Document Picker in Swift (Ios)
How Does Apple Notify iOS Apps of Refunds of In-App Purchases (Iap)
Replacement for Stringbyaddingpercentescapesusingencoding in iOS9
How to Send Data Back by Popviewcontrolleranimated for Swift
In Swift How to Call Method with Parameters on Gcd Main Thread
How to Implement Lazy Loading of Images in Table View Using Swift
How to Load an Uiimage into a Swiftui Image Asynchronously
Alamofire Invalid Value Around Character 0
How to Add iPhone 5 Large Screen Support to iOS Apps in Xcode
Remove Empty Space Before Cells in Uitableview
How to Differentiate Between Iphone4 and iPhone 3
How to Implement Uipageviewcontroller That Utilizes Multiple Viewcontrollers