[ios] Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

How do you use Auto Layout within UITableViewCells in a table view to let each cell's content and subviews determine the row height (itself/automatically), while maintaining smooth scrolling performance?

This question is related to ios uitableview autolayout row-height

The answer is


tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension

enter image description here


Let's say you have a cell with a subview, and you want the cell's height to be high enough to encompass the subview + padding.

1) Set the subview's bottom constraint equal to the cell.contentView minus the padding you want. Do not set constraints on the cell or cell.contentView itself.

2) Set either the tableView's rowHeight property or tableView:heightForRowAtIndexPath: to UITableViewAutomaticDimension.

3) Set either the tableView's estimatedRowHeight property or tableView:estimatedHeightForRowAtIndexPath: to a best guess of the height.

That's it.


Here is my solution:

You need to tell the TableView the estimatedHeight before it loads the view. Otherwise it wont be able to behave like expected.

Objective-C

- (void)viewWillAppear:(BOOL)animated {
    _messageField.delegate = self;
    _tableView.estimatedRowHeight = 65.0;
    _tableView.rowHeight = UITableViewAutomaticDimension;
}

Update to Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 65.0
}

Dynamic Table View Cell Height and Auto Layout

A good way to solve the problem with storyboard Auto Layout:

- (CGFloat)heightForImageCellAtIndexPath:(NSIndexPath *)indexPath {
  static RWImageCell *sizingCell = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier];
  });

  [sizingCell setNeedsLayout];
  [sizingCell layoutIfNeeded];

  CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  return size.height;
}

If you have a long string. e.g. one which doesn't have a line break. Then you you might run into some problems.

The "alleged" fix is mentioned by the accepted answer and few other answers. You just need to add

cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

I find Suragh's answer the most complete and concise, hence not confusing.

Though non explain why these changes are needed. Let's do that.

Drop the following code in to a project.

import UIKit

class ViewController: UIViewController {

    lazy var label : UILabel = {
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.backgroundColor = .red
        lbl.textColor = .black
        return lbl
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // step0: (0.0, 0.0)
        print("empty Text intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step1: (29.0, 20.5)
        label.text = "hiiiii"
        print("hiiiii intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step2: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints"
        print("1 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step3: (992.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints"
        print("3 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step4: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints"
        print("3 translate w/ line breaks (but the line breaks get ignored, because numberOfLines is defaulted to `1` and it will force it all to fit into one line! intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step5: (328.0, 61.0)
        label.numberOfLines = 0
        print("3 translate w/ line breaks and '0' numberOfLines intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step6: (98.5, 243.5)
        label.preferredMaxLayoutWidth = 100
        print("3 translate w/ line breaks | '0' numberOfLines | preferredMaxLayoutWidth: 100 intrinsicContentSize: \(label.intrinsicContentSize)")

        setupLayout()
    }
    func setupLayout(){
        view.addSubview(label)
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

Note that I haven't added any size constraints. I've only added centerX, centerY constraints. But still the label will be sized correctly Why?

Because of contentSize.

To better process this, first keep step0, then comment out out steps 1-6. Let setupLayout() stay. Observe the behavior.

Then uncomment step1, and observe.

Then uncomment step2 and observe.

Do this until you've uncommented all 6 steps and observed their behaviors.

What can conclude from all this? What factors can change the contenSize?

  1. Text Length: If you have a longer text then your intrinsicContentSize's width will increase
  2. Line breaks: If you add \n then the intrinsicContentSize's width will the maximum width of all lines. If one line has 25 characters, another has 2 characters and another has 21 characters then your width will be calculated based the 25 characters
  3. Number of allowed lines: You must set the numberOfLines to 0 otherwise the you won't have multiple lines. Your numberOfLines will adjust your intrinsicContentSize's height
  4. Making adjustments: Imagine that based on your text, your intrinsicContentSize's width was 200 and height was 100, but you wanted to limited the width to the label's container what are you going to do? The solution is to set it to a desired width. You do that by setting preferredMaxLayoutWidth to 130 then your new intrinsicContentSize will have a width of roughly 130. The height would obviously be more than 100 because you'd need more lines. That being said if your constraints are set correctly then you won't need to use this at all! For more on that see this answer and its comments. You only need to use preferredMaxLayoutWidth if you don't have constraints restricting the width/height as in one might say "don't wrap the text unless it exceeds the preferredMaxLayoutWidth". But with 100% certainty if you set the leading/trailing and numberOfLines to 0 then you're good! Long story short most answers here which recommend using it are WRONG! You don't need it. Needing it is a sign that your constraints are not set correctly or that you just don't have constraints

  5. Font Size: Also note that if you increase your fontSize then the intrinsicContentSize's height will increase. I didn't show that in my code. You can try that on your own.

So back to your tableViewCell example:

All you need to do is:

  • set the numberOfLines to 0
  • constrain the label correctly to the margins/edges
  • There is no need to set preferredMaxLayoutWidth.

With regard to the accepted answer by @smileyborg, I have found

[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]

to be unreliable in some cases where constraints are ambiguous. Better to force the layout engine to calculate the height in one direction, by using the helper category on UIView below:

-(CGFloat)systemLayoutHeightForWidth:(CGFloat)w{
    [self setNeedsLayout];
    [self layoutIfNeeded];
    CGSize size = [self systemLayoutSizeFittingSize:CGSizeMake(w, 1) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGFloat h = size.height;
    return h;
}

Where w: is the width of the tableview


Swift example of a variable height UITableViewCell

Updated for Swift 3

William Hu's Swift answer is good, but it helps me to have some simple yet detailed steps when learning to do something for the first time. The example below is my test project while learning to make a UITableView with variable cell heights. I based it on this basic UITableView example for Swift.

The finished project should look like this:

enter image description here

Create a new project

It can be just a Single View Application.

Add the code

Add a new Swift file to your project. Name it MyCustomCell. This class will hold the outlets for the views that you add to your cell in the storyboard. In this basic example we will only have one label in each cell.

import UIKit
class MyCustomCell: UITableViewCell {
    @IBOutlet weak var myCellLabel: UILabel!
}

We will connect this outlet later.

Open ViewController.swift and make sure you have the following content:

import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    // These strings will be the data for the table view cells
    let animals: [String] = [
        "Ten horses:  horse horse horse horse horse horse horse horse horse horse ",
        "Three cows:  cow, cow, cow",
        "One camel:  camel",
        "Ninety-nine sheep:  sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep baaaa sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep",
        "Thirty goats:  goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat "]

    // Don't forget to enter this in IB also
    let cellReuseIdentifier = "cell"

    @IBOutlet var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // delegate and data source
        tableView.delegate = self
        tableView.dataSource = self

        // Along with auto layout, these are the keys for enabling variable cell height
        tableView.estimatedRowHeight = 44.0
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    // number of rows in table view
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.animals.count
    }

    // create a cell for each table view row
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell:MyCustomCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MyCustomCell
        cell.myCellLabel.text = self.animals[indexPath.row]
        return cell
    }

    // method to run when table view cell is tapped
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("You tapped cell number \(indexPath.row).")
    }
}

Important Note:

  • It is the following two lines of code (along with auto layout) that make the variable cell height possible:

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension
    

Setup the storyboard

Add a Table View to your view controller and use auto layout to pin it to the four sides. Then drag a Table View Cell onto the Table View. And onto the Prototype cell, drag a Label. Use auto layout to pin the label to the four edges of the content view of the Table View Cell.

enter image description here

Important note:

  • Auto layout works together with the important two lines of code I mentioned above. If you don't use auto layout it isn't going to work.

Other IB settings

Custom class name and Identifier

Select the Table View Cell and set the custom class to be MyCustomCell (the name of the class in the Swift file we added). Also set the Identifier to be cell (the same string that we used for the cellReuseIdentifier in the code above.

enter image description here

Zero Lines for Label

Set the number of lines to 0 in your Label. This means multi-line and allows the label to resize itself based on its content.

enter image description here

Hook Up the Outlets

  • Control drag from the Table View in the storyboard to the tableView variable in the ViewController code.
  • Do the same for the Label in your Prototype cell to the myCellLabel variable in the MyCustomCell class.

Finished

You should be able to run your project now and get cells with variable heights.

Notes

  • This example only works for iOS 8 and after. If you are still needing to support iOS 7 then this won't work for you.
  • Your own custom cells in your future projects will probably have more than a single label. Make sure that you get everything pinned right so that auto layout can determine the correct height to use. You may also have to use vertical compression resistance and hugging. See this article for more about that.
  • If you are not pinning the leading and trailing (left and right) edges, you may also need to set the label's preferredMaxLayoutWidth so that it knows when to line wrap. For example, if you had added a Center Horizontally constraint to the label in the project above rather than pin the leading and trailing edges, then you would need to add this line to the tableView:cellForRowAtIndexPath method:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
    

See also


The solution proposed by @smileyborg is almost perfect. If you have a custom cell and you want one or more UILabel with dynamic heights then the systemLayoutSizeFittingSize method combined with AutoLayout enabled returns a CGSizeZero unless you move all your cell constraints from the cell to its contentView (as suggested by @TomSwift here How to resize superview to fit all subviews with autolayout?).

To do so you need to insert the following code in your custom UITableViewCell implementation (thanks to @Adrian).

- (void)awakeFromNib{
    [super awakeFromNib];
    for (NSLayoutConstraint *cellConstraint in self.constraints) {
        [self removeConstraint:cellConstraint];
        id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
        id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
        NSLayoutConstraint *contentViewConstraint =
        [NSLayoutConstraint constraintWithItem:firstItem
                                 attribute:cellConstraint.firstAttribute
                                 relatedBy:cellConstraint.relation
                                    toItem:seccondItem
                                 attribute:cellConstraint.secondAttribute
                                multiplier:cellConstraint.multiplier
                                  constant:cellConstraint.constant];
        [self.contentView addConstraint:contentViewConstraint];
    }
}

Mixing @smileyborg answer with this should works.


In my case i have to create a custom cell with a image which is coming from server and can be of any width and height. And two UILabels with dynamic size(both width & height)

i have achieved the same here in my answer with autolayout and programmatically:

Basically above @smileyBorg answer helped but systemLayoutSizeFittingSize never worked for me, In my approach :

1. No use of automatic row height calculation property. 2.No use of estimated height 3.No need of unnecessary updateConstraints. 4.No use of Automatic Preferred Max Layout Width. 5. No use of systemLayoutSizeFittingSize (should have use but not working for me, i dont know what it is doing internally), but instead my method -(float)getViewHeight working and i know what it's doing internally.

Is it possible to have differing heights in a UITableView Cell when I use several different ways of displaying the cell?


To set automatic dimension for row height & estimated row height, ensure following steps to make, auto dimension effective for cell/row height layout.

  • Assign and implement tableview dataSource and delegate
  • Assign UITableViewAutomaticDimension to rowHeight & estimatedRowHeight
  • Implement delegate/dataSource methods (i.e. heightForRowAt and return a value UITableViewAutomaticDimension to it)

-

Objective C:

// in ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>

  @property IBOutlet UITableView * table;

@end

// in ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.table.dataSource = self;
    self.table.delegate = self;

    self.table.rowHeight = UITableViewAutomaticDimension;
    self.table.estimatedRowHeight = UITableViewAutomaticDimension;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return UITableViewAutomaticDimension;
}

Swift:

@IBOutlet weak var table: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    // Don't forget to set dataSource and delegate for table
    table.dataSource = self
    table.delegate = self

    // Set automatic dimensions for row height
    // Swift 4.2 onwards
    table.rowHeight = UITableView.automaticDimension
    table.estimatedRowHeight = UITableView.automaticDimension


    // Swift 4.1 and below
    table.rowHeight = UITableViewAutomaticDimension
    table.estimatedRowHeight = UITableViewAutomaticDimension

}



// UITableViewAutomaticDimension calculates height of label contents/text
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    // Swift 4.2 onwards
    return UITableView.automaticDimension

    // Swift 4.1 and below
    return UITableViewAutomaticDimension
}

For label instance in UITableviewCell

  • Set number of lines = 0 (& line break mode = truncate tail)
  • Set all constraints (top, bottom, right left) with respect to its superview/ cell container.
  • Optional: Set minimum height for label, if you want minimum vertical area covered by label, even if there is no data.

enter image description here

Note: If you've more than one labels (UIElements) with dynamic length, which should be adjusted according to its content size: Adjust 'Content Hugging and Compression Resistance Priority` for labels which you want to expand/compress with higher priority.


swift 4

    @IBOutlet weak var tableViewHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var tableView: UITableView!
    private var context = 1
 override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.addObserver(self, forKeyPath: "contentSize", options: [.new,.prior], context: &context)
    }
  // Added observer to adjust tableview height based on the content

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &self.context{
            if let size = change?[NSKeyValueChangeKey.newKey] as? CGSize{
                print("-----")
                print(size.height)
                tableViewHeightConstraint.constant = size.height + 50
            }
        }
    }

//Remove observer
 deinit {

        NotificationCenter.default.removeObserver(self)

    }

As long as your layout in your cell is good.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];

    return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}

Update: You should use dynamic resizing introduced in iOS 8.


I just did some dumb try and error with the 2 values of rowHeight and estimatedRowHeight and just thought it might provide some debugging insight:

If you set them both OR only set the estimatedRowHeight you will get the desired behavior:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1.00001 // MUST be greater than 1

It's suggested that you do your best to get the correct estimate, but the end result isn't different. It will just affect your performance.

enter image description here


If you only set the rowHeight ie only do:

tableView.rowHeight = UITableViewAutomaticDimension

your end result would not be as desired:

enter image description here


If you set the estimatedRowHeight to 1 or smaller then you will crash regardless of the rowHeight.

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1 

I crashed with the following error message:

Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'table view row height
must not be negative - provided height for index path (<NSIndexPath:
0xc000000000000016> {length = 2, path = 0 - 0}) is -1.000000'
    ...some other lines...

libc++abi.dylib: terminating with uncaught exception of type
NSException

(for Xcode 8.x / Xcode 9.x read at the bottom)

Beware of the following issue in in Xcode 7.x, which might be a source of confusion:

Interface Builder does not handle auto-sizing cell set-up properly. Even if your constraints are absolutely valid, IB will still complain and give you confusing suggestions and errors. The reason is that IB is unwilling to change the row's height as your constraints dictate (so that the cell fits around your content). Instead, it keeps the row's height fixed and starts suggesting you change your constraints, which you should ignore.

For example, imagine you've set up everything fine, no warnings, no errors, all works.

enter image description here

Now if you change the font size (in this example I'm changing the description label font size from 17.0 to 18.0).

enter image description here

Because the font size increased, the label now wants to occupy 3 rows (before that it was occupying 2 rows).

If Interface Builder worked as expected, it would resize the cell's height to accommodate the new label height. However what actually happens is that IB displays the red auto-layout error icon and suggest that you modify hugging/compression priorities.

enter image description here

You should ignore these warnings. What you can* do instead is to manually change the row's height in (select Cell > Size Inspector > Row Height).

enter image description here

I was changing this height one click at a time (using the up/down stepper) until the red arrow errors disappear! (you will actually get yellow warnings, at which point just go ahead and do 'update frames', it should all work).

* Note that you don't actually have to resolve these red errors or yellow warnings in Interface Builder - at runtime, everything will work correctly (even if IB shows errors/warnings). Just make sure that at runtime in the console log you're not getting any AutoLayout errors.

In fact trying to always update row height in IB is super annoying and sometimes close to impossible (because of fractional values).

To prevent the annoying IB warnings/errors, you can select the views involved and in Size Inspector for the property Ambiguity choose Verify Position Only

enter image description here


Xcode 8.x / Xcode 9.x seems to (sometimes) be doing things differently than Xcode 7.x, but still incorrectly. For example even when compression resistance priority / hugging priority are set to required (1000), Interface Builder might stretch or clip a label to fit the cell (instead of resizing cell height to fit around the label). And in such a case it might not even show any AutoLayout warnings or errors. Or sometimes it does exactly what Xcode 7.x did, described above.


If the cell height is dynamic by the content, you should precisely count it out and then return the height value before the cell is rendered. An easy way is to define the counting method in the table view cell code for controller to call at the table cell height delegate method. Don't forget to count out the real cell frame width (default is 320) if the height is rely on the width of the table or screen. That is, in the table cell height delegate method, use cell.frame to correct the cell width first, then call the counting height method defined in the cell to get the suitable value and return it.

PS. The code for generating cell object could be defined in another method for different table view cell delegate method to call.


If you do you layout programmatically, here is what to consider for iOS 10 using anchors in Swift.

There are three rules/ steps

NUMBER 1: set this two properties of tableview on viewDidLoad, the first one is telling to the tableview that should expect dynamic sizes on their cells, the second one is just to let the app calculate the size of the scrollbar indicator, so it helps for performance.

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100

NUMBER 2: This is important you need to add the subviews to the contentView of the cell not to the view, and also use its layoutsmarginguide to anchor the subviews to the top and bottom, this is a working example of how to do it.

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    setUpViews()
}

private func setUpViews() {

    contentView.addSubview(movieImageView)
    contentView.addSubview(descriptionLabel)
    let marginGuide = contentView.layoutMarginsGuide

    NSLayoutConstraint.activate([
        movieImageView.heightAnchor.constraint(equalToConstant: 80),
        movieImageView.widthAnchor.constraint(equalToConstant: 80),
        movieImageView.leftAnchor.constraint(equalTo: marginGuide.leftAnchor),
        movieImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 20),

        descriptionLabel.leftAnchor.constraint(equalTo: movieImageView.rightAnchor, constant: 15),
        descriptionLabel.rightAnchor.constraint(equalTo: marginGuide.rightAnchor),
        descriptionLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -15),
        descriptionLabel.topAnchor.constraint(equalTo: movieImageView.topAnchor)

        ])
}

Create a method that will add the subviews and perform the layout, call it in the init method.

NUMBER 3: DON'T CALL THE METHOD:

  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    }

If you do it you will override your implementation.

Follow this 3 rules for dynamic cells in tableviews.

here is a working implementation https://github.com/jamesrochabrun/MinimalViewController


In case people are still having trouble with this. I wrote a quick blog post about using Autolayout with UITableViews Leveraging Autolayout For Dynamic Cell Heights as well as an open source component to help make this more abstract and easier to implement. https://github.com/Raizlabs/RZCellSizeManager


yet another iOs7+iOs8 solution in Swift

var cell2height:CGFloat=44

override func viewDidLoad() {
    super.viewDidLoad()
    theTable.rowHeight = UITableViewAutomaticDimension
    theTable.estimatedRowHeight = 44.0;
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell =  tableView.dequeueReusableCellWithIdentifier("myTableViewCell", forIndexPath: indexPath) as! myTableViewCell
    cell2height=cell.contentView.height
    return cell
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    if #available(iOS 8.0, *) {
        return UITableViewAutomaticDimension
    } else {
        return cell2height
    }
}

Simply add these two functions in your viewcontroller it will solve your problem. Here, list is a string array which contain your string of every row.

 func tableView(_ tableView: UITableView, 
   estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        tableView.rowHeight = self.calculateHeight(inString: list[indexPath.row])

    return (tableView.rowHeight) 
}

func calculateHeight(inString:String) -> CGFloat
{
    let messageString = input.text
    let attributes : [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue) : UIFont.systemFont(ofSize: 15.0)]

    let attributedString : NSAttributedString = NSAttributedString(string: messageString!, attributes: attributes)

    let rect : CGRect = attributedString.boundingRect(with: CGSize(width: 222.0, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)

    let requredSize:CGRect = rect
    return requredSize.height
}

An important enough gotcha I just ran into to post as an answer.

@smileyborg's answer is mostly correct. However, if you have any code in the layoutSubviews method of your custom cell class, for instance setting the preferredMaxLayoutWidth, then it won't be run with this code:

[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];

It confounded me for awhile. Then I realized it's because those are only triggering layoutSubviews on the contentView, not the cell itself.

My working code looks like this:

TCAnswerDetailAppSummaryCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailAppSummaryCell"];
[cell configureWithThirdPartyObject:self.app];
[cell layoutIfNeeded];
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height;

Note that if you are creating a new cell, I'm pretty sure you don't need to call setNeedsLayout as it should already be set. In cases where you save a reference to a cell, you should probably call it. Either way it shouldn't hurt anything.

Another tip if you are using cell subclasses where you are setting things like preferredMaxLayoutWidth. As @smileyborg mentions, "your table view cell hasn't yet had its width fixed to the table view's width". This is true, and trouble if you are doing your work in your subclass and not in the view controller. However you can simply set the cell frame at this point using the table width:

For instance in the calculation for height:

self.summaryCell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailDefaultSummaryCell"];
CGRect oldFrame = self.summaryCell.frame;
self.summaryCell.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, self.tableView.frame.size.width, oldFrame.size.height);

(I happen to cache this particular cell for re-use, but that's irrelevant).


I had to use dynamic views (setup views and constraints by code) and when I wanted to set preferredMaxLayoutWidth label's width was 0. So I've got wrong cell height.

Then I added

[cell layoutSubviews];

before executing

[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];

After that label's width was as expected and dynamic height was calculating right.


I wrapped @smileyborg's iOS7 solution in a category

I decided to wrap this clever solution by @smileyborg into a UICollectionViewCell+AutoLayoutDynamicHeightCalculation category.

The category also rectifies the issues outlined in @wildmonkey's answer (loading a cell from a nib and systemLayoutSizeFittingSize: returning CGRectZero)

It doesn't take into account any caching but suits my needs right now. Feel free to copy, paste and hack at it.

UICollectionViewCell+AutoLayoutDynamicHeightCalculation.h

#import <UIKit/UIKit.h>

typedef void (^UICollectionViewCellAutoLayoutRenderBlock)(void);

/**
 *  A category on UICollectionViewCell to aid calculating dynamic heights based on AutoLayout contraints.
 *
 *  Many thanks to @smileyborg and @wildmonkey
 *
 *  @see stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights
 */
@interface UICollectionViewCell (AutoLayoutDynamicHeightCalculation)

/**
 *  Grab an instance of the receiving type to use in order to calculate AutoLayout contraint driven dynamic height. The method pulls the cell from a nib file and moves any Interface Builder defined contrainsts to the content view.
 *
 *  @param name Name of the nib file.
 *
 *  @return collection view cell for using to calculate content based height
 */
+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name;

/**
 *  Returns the height of the receiver after rendering with your model data and applying an AutoLayout pass
 *
 *  @param block Render the model data to your UI elements in this block
 *
 *  @return Calculated constraint derived height
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width;

/**
 *  Directly calls `heightAfterAutoLayoutPassAndRenderingWithBlock:collectionViewWidth` assuming a collection view width spanning the [UIScreen mainScreen] bounds
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block;

@end

UICollectionViewCell+AutoLayoutDynamicHeightCalculation.m

#import "UICollectionViewCell+AutoLayout.h"

@implementation UICollectionViewCell (AutoLayout)

#pragma mark Dummy Cell Generator

+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name
{
    UICollectionViewCell *heightCalculationCell = [[[NSBundle mainBundle] loadNibNamed:name owner:self options:nil] lastObject];
    [heightCalculationCell moveInterfaceBuilderLayoutConstraintsToContentView];
    return heightCalculationCell;
}

#pragma mark Moving Constraints

- (void)moveInterfaceBuilderLayoutConstraintsToContentView
{
    [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
        [self removeConstraint:constraint];
        id firstItem = constraint.firstItem == self ? self.contentView : constraint.firstItem;
        id secondItem = constraint.secondItem == self ? self.contentView : constraint.secondItem;
        [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:firstItem
                                                                     attribute:constraint.firstAttribute
                                                                     relatedBy:constraint.relation
                                                                        toItem:secondItem
                                                                     attribute:constraint.secondAttribute
                                                                    multiplier:constraint.multiplier
                                                                      constant:constraint.constant]];
    }];
}

#pragma mark Height

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block
{
    return [self heightAfterAutoLayoutPassAndRenderingWithBlock:block
                                            collectionViewWidth:CGRectGetWidth([[UIScreen mainScreen] bounds])];
}

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width
{
    NSParameterAssert(block);

    block();

    [self setNeedsUpdateConstraints];
    [self updateConstraintsIfNeeded];

    self.bounds = CGRectMake(0.0f, 0.0f, width, CGRectGetHeight(self.bounds));

    [self setNeedsLayout];
    [self layoutIfNeeded];

    CGSize calculatedSize = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

    return calculatedSize.height;

}

@end

Usage example:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    MYSweetCell *cell = [MYSweetCell heightCalculationCellFromNibWithName:NSStringFromClass([MYSweetCell class])];
    CGFloat height = [cell heightAfterAutoLayoutPassAndRenderingWithBlock:^{
        [(id<MYSweetCellRenderProtocol>)cell renderWithModel:someModel];
    }];
    return CGSizeMake(CGRectGetWidth(self.collectionView.bounds), height);
}

Thankfully we won't have to do this jazz in iOS8, but there it is for now!


UITableView.automaticDimension can be set via Interface Builder:

Xcode > Storyboard > Size Inspector

Table View Cell > Row Height > Automatic

Size Inspector


In my case, the padding was because of the sectionHeader and sectionFooter heights, where storyboard allowed me to change it to minimum 1. So in viewDidLoad method:

tableView.sectionHeaderHeight = 0
tableView.sectionFooterHeight = 0

For iOS 8 above it's really simple:

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableView.automaticDimension
}

or

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableView.automaticDimension
}

But for iOS 7, the key is calculate the height after autolayout:

func calculateHeightForConfiguredSizingCell(cell: GSTableViewCell) -> CGFloat {
    cell.setNeedsLayout()
    cell.layoutIfNeeded()
    let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingExpandedSize).height + 1.0
    return height
}

Important

  • If multiple lines labels, don't forget set the numberOfLines to 0.

  • Don't forget label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

The full example code is here.


Another "solution": skip all this frustration and use a UIScrollView instead to get a result that looks and feels identical to UITableView.

That was the painful "solution" for me, after having put in literally 20+ very frustrating hours total trying to build something like what smileyborg suggested and failing over many months and three versions of App Store releases.

My take is that if you really need iOS 7 support (for us, it's essential) then the technology is just too brittle and you'll pull your hair out trying. And that UITableView is complete overkill generally unless you're using some of the advanced row editing features and/or really need to support 1000+ "rows" (in our app, it's realistically never more than 20 rows).

The added bonus is that the code gets insanely simple versus all the delegate crap and back and forth that comes with UITableView. It's just one single loop of code in viewOnLoad that looks elegant and is easy to manage.

Here's some tips on how to do it:

  1. Using either Storyboard or a nib file, create a ViewController and associated root view.

  2. Drag over a UIScrollView onto your root view.

  3. Add constraints top, bottom, left and right constraints to the top-level view so the UIScrollView fills the entire root view.

  4. Add a UIView inside the UIScrollView and call it "container". Add top, bottom, left and right constraints to the UIScrollView (its parent). KEY TRICK: Also add a "Equal widths" constraints to link the UIScrollView and UIView.

    NOTE: You will get an error "scroll view has ambiguous scrollable content height" and that your container UIView should have a height of 0 pixels. Neither error seems to matter when the app is running.

  5. Create nib files and controllers for each of your "cells". Use UIView not UITableViewCell.

  6. In your root ViewController, you essentially add all the "rows" to the container UIView and programmatically add constraints linking their left and right edges to the container view, their top edges to either the container view top (for the first item) or the previous cell. Then link the final cell to the container bottom.

For us, each "row" is in a nib file. So the code looks something like this:

class YourRootViewController {

    @IBOutlet var container: UIView! //container mentioned in step 4

    override func viewDidLoad() {
        
        super.viewDidLoad()

        var lastView: UIView?
        for data in yourDataSource {

            var cell = YourCellController(nibName: "YourCellNibName", bundle: nil)
            UITools.addViewToTop(container, child: cell.view, sibling: lastView)
            lastView = cell.view
            //Insert code here to populate your cell
        }

        if(lastView != nil) {
            container.addConstraint(NSLayoutConstraint(
                item: lastView!,
                attribute: NSLayoutAttribute.Bottom,
                relatedBy: NSLayoutRelation.Equal,
                toItem: container,
                attribute: NSLayoutAttribute.Bottom,
                multiplier: 1,
                constant: 0))
        }

        ///Add a refresh control, if you want - it seems to work fine in our app:
        var refreshControl = UIRefreshControl()
        container.addSubview(refreshControl!)
    }
}

And here's the code for UITools.addViewToTop:

class UITools {
    ///Add child to container, full width of the container and directly under sibling (or container if sibling nil):
    class func addViewToTop(container: UIView, child: UIView, sibling: UIView? = nil)
    {
        child.setTranslatesAutoresizingMaskIntoConstraints(false)
        container.addSubview(child)
        
        //Set left and right constraints so fills full horz width:
        
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Leading,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Left,
            multiplier: 1,
            constant: 0))
        
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Trailing,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Right,
            multiplier: 1,
            constant: 0))
        
        //Set vertical position from last item (or for first, from the superview):
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Top,
            relatedBy: NSLayoutRelation.Equal,
            toItem: sibling == nil ? container : sibling,
            attribute: sibling == nil ? NSLayoutAttribute.Top : NSLayoutAttribute.Bottom,
            multiplier: 1,
            constant: 0))
    }
}

The only "gotcha" I've found with this approach so far is that UITableView has a nice feature of "floating" section headers at the top of the view as you scroll. The above solution won't do that unless you add more programming but for our particular case this feature wasn't 100% essential and nobody noticed when it went away.

If you want dividers between your cells, just add a 1 pixel high UIView at the bottom of your custom "cell" that looks like a divider.

Be sure to turn on "bounces" and "bounce vertically" for the refresh control to work and so it seems more like a tableview.

TableView shows some empty rows and dividers under your content, if it doesn't fill the full screen where as this solution doesn't. But personally, I prefer if those empty rows weren't there anyway - with variable cell height it always looked "buggy" to me anyway to have the empty rows in there.

Here's hoping some other programmer reads my post BEFORE wasting 20+ hours trying to figure it out with Table View in their own app. :)


Like @Bob-Spryn I ran into an important enough gotcha that I'm posting this as an answer.

I struggled with @smileyborg's answer for a while. The gotcha that I ran into is if you've defined your prototype cell in IB with additional elements (UILabels, UIButtons, etc.) in IB when you instantiate the cell with [[YourTableViewCellClass alloc] init] it will not instantiate all the other elements within that cell unless you've written code to do that. (I had a similar experience with initWithStyle.)

To have the storyboard instantiate all the additional elements obtain your cell with [tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"] (Not [tableView dequeueReusableCellWithIdentifier:forIndexPath:] as this'll cause interesting problems.) When you do this all the elements you defined in IB will be instantiated.


Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to uitableview

Adding a UISegmentedControl to UITableView make UITableViewCell selectable only while editing How to fix Error: this class is not key value coding-compliant for the key tableView.' UITableView example for Swift Swift - how to make custom header for UITableView? How to insert new cell into UITableView in Swift How to detect tableView cell touched or clicked in swift Dynamic Height Issue for UITableView Cells (Swift) UIButton action in table view cell How to get the indexpath.row when an element is activated?

Examples related to autolayout

Autoresize View When SubViews are Added How to update the constant height constraint of a UIView programmatically? Getting a "This application is modifying the autolayout engine from a background thread" error? Programmatically Add CenterX/CenterY Constraints How to trap on UIViewAlertForUnsatisfiableConstraints? How to add constraints programmatically using Swift UICollectionView Self Sizing Cells with Auto Layout Automatic Preferred Max Layout Width is not available on iOS versions prior to 8.0 Remove all constraints affecting a UIView How do I programmatically set device orientation in iOS 7?

Examples related to row-height

Dynamic Height Issue for UITableView Cells (Swift) Using Auto Layout in UITableView for dynamic cell layouts & variable row heights Setting custom UITableViewCells height