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?

upvote
  flag
Here's the sample code in swift 2.3 github.com/dpakthakur/DynamicCellHeight – Deepak Thakur

26 Answers 11

up vote 2197 down vote accepted

TL;DR: Don't like reading? Jump straight to the sample projects on GitHub:

Conceptual Description

The first 2 steps below are applicable regardless of which iOS versions you are developing for.

1. Set Up & Add Constraints

In your UITableViewCell subclass, add constraints so that the subviews of the cell have their edges pinned to the edges of the cell's contentView (most importantly to the top AND bottom edges). NOTE: don't pin subviews to the cell itself; only to the cell's contentView! Let the intrinsic content size of these subviews drive the height of the table view cell's content view by making sure the content compression resistance and content hugging constraints in the vertical dimension for each subview are not being overridden by higher-priority constraints you have added. (Huh? Click here.)

Remember, the idea is to have the cell's subviews connected vertically to the cell's content view so that they can "exert pressure" and make the content view expand to fit them. Using an example cell with a few subviews, here is a visual illustration of what some (not all!) of your constraints would need to look like:

Example illustration of constraints on a table view cell.

You can imagine that as more text is added to the multi-line body label in the example cell above, it will need to grow vertically to fit the text, which will effectively force the cell to grow in height. (Of course, you need to get the constraints right in order for this to work correctly!)

Getting your constraints right is definitely the hardest and most important part of getting dynamic cell heights working with Auto Layout. If you make a mistake here, it could prevent everything else from working -- so take your time! I recommend setting up your constraints in code because you know exactly which constraints are being added where, and it's a lot easier to debug when things go wrong. Adding constraints in code is just as easy as and significantly more powerful than Interface Builder when you leverage one of the fantastic open source APIs available -- here is the one I design, maintain, and use exclusively: https://github.com/smileyborg/PureLayout

  • If you're adding constraints in code, you should do this once from within the updateConstraints method of your UITableViewCell subclass. Note that updateConstraints may be called more than once, so to avoid adding the same constraints more than once, make sure to wrap your constraint-adding code within updateConstraints in a check for a boolean property such as didSetupConstraints (which you set to YES after you run your constraint-adding code once). On the other hand, if you have code that updates existing constraints (such as adjusting the constant property on some constraints), place this in updateConstraints but outside of the check for didSetupConstraints so it can run every time the method is called.

2. Determine Unique Table View Cell Reuse Identifiers

For every unique set of constraints in the cell, use a unique cell reuse identifier. In other words, if your cells have more than one unique layout, each unique layout should receive its own reuse identifier. (A good hint that you need to use a new reuse identifier is when your cell variant has a different number of subviews, or the subviews are arranged in a distinct fashion.)

For example, if you were displaying an email message in each cell, you might have 4 unique layouts: messages with just a subject, messages with a subject and a body, messages with a subject and a photo attachment, and messages with a subject, body, and photo attachment. Each layout has completely different constraints required to achieve it, so once the cell is initialized and the constraints are added for one of these cell types, the cell should get a unique reuse identifier specific to that cell type. This means when you dequeue a cell for reuse, the constraints have already been added and are ready to go for that cell type.

Note that due to differences in intrinsic content size, cells with the same constraints (type) may still have varying heights! Don't confuse fundamentally different layouts (different constraints) with different calculated view frames (solved from identical constraints) due to different sizes of content.

  • Do not add cells with completely different sets of constraints to the same reuse pool (i.e. use the same reuse identifier) and then attempt to remove the old constraints and set up new constraints from scratch after each dequeue. The internal Auto Layout engine is not designed to handle large scale changes in constraints, and you will see massive performance issues.

For iOS 8 - Self-Sizing Cells

3. Enable Row Height Estimation

To enable self-sizing table view cells, you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property. As soon as both of these properties are set, the system uses Auto Layout to calculate the row’s actual height

Apple: Working with Self-Sizing Table View Cells

With iOS 8, Apple has internalized much of the work that previously had to be implemented by you prior to iOS 8. In order to allow the self-sizing cell mechanism to work, you must first set the rowHeight property on the table view to the constant UITableViewAutomaticDimension. Then, you simply need to enable row height estimation by setting the table view's estimatedRowHeight property to a nonzero value, for example:

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is

What this does is provide the table view with a temporary estimate/placeholder for the row heights of cells that are not yet onscreen. Then, when these cells are about to scroll onscreen, the actual row height will be calculated. To determine the actual height for each row, the table view automatically asks each cell what height its contentView needs to be based on the known fixed width of the content view (which is based on the table view's width, minus any additional things like a section index or accessory view) and the auto layout constraints you have added to the cell's content view and subviews. Once this actual cell height has been determined, the old estimated height for the row is updated with the new actual height (and any adjustments to the table view's contentSize/contentOffset are made as needed for you).

Generally speaking, the estimate you provide doesn't have to be very accurate -- it is only used to correctly size the scroll indicator in the table view, and the table view does a good job of adjusting the scroll indicator for incorrect estimates as you scroll cells onscreen. You should set the estimatedRowHeight property on the table view (in viewDidLoad or similar) to a constant value that is the "average" row height. Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing tableView:estimatedHeightForRowAtIndexPath: to do the minimal calculation required to return a more accurate estimate for each row.

For iOS 7 support (implementing auto cell sizing yourself)

3. Do a Layout Pass & Get The Cell Height

First, instantiate an offscreen instance of a table view cell, one instance for each reuse identifier, that is used strictly for height calculations. (Offscreen meaning the cell reference is stored in a property/ivar on the view controller and never returned from tableView:cellForRowAtIndexPath: for the table view to actually render onscreen.) Next, the cell must be configured with the exact content (e.g. text, images, etc) that it would hold if it were to be displayed in the table view.

Then, force the cell to immediately layout its subviews, and then use the systemLayoutSizeFittingSize: method on the UITableViewCell's contentView to find out what the required height of the cell is. Use UILayoutFittingCompressedSize to get the smallest size required to fit all the contents of the cell. The height can then be returned from the tableView:heightForRowAtIndexPath: delegate method.

4. Use Estimated Row Heights

If your table view has more than a couple dozen rows in it, you will find that doing the Auto Layout constraint solving can quickly bog down the main thread when first loading the table view, as tableView:heightForRowAtIndexPath: is called on each and every row upon first load (in order to calculate the size of the scroll indicator).

As of iOS 7, you can (and absolutely should) use the estimatedRowHeight property on the table view. What this does is provide the table view with a temporary estimate/placeholder for the row heights of cells that are not yet onscreen. Then, when these cells are about to scroll onscreen, the actual row height will be calculated (by calling tableView:heightForRowAtIndexPath:), and the estimated height updated with the actual one.

Generally speaking, the estimate you provide doesn't have to be very accurate -- it is only used to correctly size the scroll indicator in the table view, and the table view does a good job of adjusting the scroll indicator for incorrect estimates as you scroll cells onscreen. You should set the estimatedRowHeight property on the table view (in viewDidLoad or similar) to a constant value that is the "average" row height. Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing tableView:estimatedHeightForRowAtIndexPath: to do the minimal calculation required to return a more accurate estimate for each row.

5. (If Needed) Add Row Height Caching

If you've done all the above and are still finding that performance is unacceptably slow when doing the constraint solving in tableView:heightForRowAtIndexPath:, you'll unfortunately need to implement some caching for cell heights. (This is the approach suggested by Apple's engineers.) The general idea is to let the Auto Layout engine solve the constraints the first time, then cache the calculated height for that cell and use the cached value for all future requests for that cell's height. The trick of course is to make sure you clear the cached height for a cell when anything happens that could cause the cell's height to change -- primarily, this would be when that cell's content changes or when other important events occur (like the user adjusting the Dynamic Type text size slider).

iOS 7 Generic Sample Code (with lots of juicy comments)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path, depending on the particular layout required (you may have
    // just one, or may have many).
    NSString *reuseIdentifier = ...;

    // Dequeue a cell for the reuse identifier.
    // Note that this method will init and return a new cell if there isn't
    // one available in the reuse pool, so either way after this line of 
    // code you will have a cell with the correct constraints ready to go.
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];

    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...

    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method: [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // If you are using multi-line UILabels, don't forget that the 
    // preferredMaxLayoutWidth needs to be set correctly. Do it at this 
    // point if you are NOT doing it within the UITableViewCell subclass 
    // -[layoutSubviews] method. For example: 
    // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path.
    NSString *reuseIdentifier = ...;

    // Use a dictionary of offscreen cells to get a cell for the reuse 
    // identifier, creating a cell and storing it in the dictionary if one 
    // hasn't already been added for the reuse identifier. WARNING: Don't 
    // call the table view's dequeueReusableCellWithIdentifier: method here 
    // because this will result in a memory leak as the cell is created but 
    // never returned from the tableView:cellForRowAtIndexPath: method!
    UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
    if (!cell) {
        cell = [[YourTableViewCellClass alloc] init];
        [self.offscreenCells setObject:cell forKey:reuseIdentifier];
    }

    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...

    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // Set the width of the cell to match the width of the table view. This
    // is important so that we'll get the correct cell height for different
    // table view widths if the cell's height depends on its width (due to 
    // multi-line UILabels word wrapping, etc). We don't need to do this 
    // above in -[tableView:cellForRowAtIndexPath] because it happens 
    // automatically when the cell is used in the table view. Also note, 
    // the final width of the cell may not be the width of the table view in
    // some cases, for example when a section index is displayed along 
    // the right side of the table view. You must account for the reduced 
    // cell width.
    cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));

    // Do the layout pass on the cell, which will calculate the frames for 
    // all the views based on the constraints. (Note that you must set the 
    // preferredMaxLayoutWidth on multi-line UILabels inside the 
    // -[layoutSubviews] method of the UITableViewCell subclass, or do it 
    // manually at this point before the below 2 lines!)
    [cell setNeedsLayout];
    [cell layoutIfNeeded];

    // Get the actual height required for the cell's contentView
    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    // Add an extra point to the height to account for the cell separator, 
    // which is added between the bottom of the cell's contentView and the 
    // bottom of the table view cell.
    height += 1.0f;

    return height;
}

// NOTE: Set the table view's estimatedRowHeight property instead of 
// implementing the below method, UNLESS you have extreme variability in 
// your row heights and you notice the scroll indicator "jumping" 
// as you scroll.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Do the minimal calculations required to be able to return an 
    // estimated row height that's within an order of magnitude of the 
    // actual height. For example:
    if ([self isTallCellAtIndexPath:indexPath]) {
        return 350.0f;
    } else {
        return 40.0f;
    }
}

Sample Projects

These projects are fully working examples of table views with variable row heights due to table view cells containing dynamic content in UILabels.

Feel free to raise any questions or issues you run into (you can open issues on GitHub or post comments here). I'll try my best to help!

Xamarin (C#/.NET)

If you're using Xamarin, check out this sample project put together by @KentBoogaart.

upvote
  flag
How about those performance tricks now that the NDA is lifted? – Jens Utbult
1 upvote
  flag
Done! Please let me know if you have any questions, and I would love to hear feedback from other folks using Auto Layout in table views (especially for complicated cases like I describe where you have multiple constraint setups). – smileyborg
1 upvote
  flag
While this is working well, I find it under estimates the required sizes slightly (possibly due to various rounding issues) and I have to add a couple points to the final height in order for all my text to fit inside the labels – DBD
upvote
  flag
@DBD I haven't seen this, and I've used this approach now for at least a dozen different table views. Without any other info I would guess it's an issue with your constraints -- the intrinsic content size constraints for UILabel should definitely solve to an accurate size (where the text doesn't get clipped). Maybe post a question here or shoot me some more info and we can figure it out? – smileyborg
upvote
  flag
@caoimghgin I've forked your project on GitHub and will fix up a few things, then commit the changes so you can take a look. Realized I should clarify some of the example code in my answer here too! – smileyborg
upvote
  flag
@caoimghgin I've opened a pull request on GitHub with the changes I made. Added comments in the code to explain what I did too. You definitely had it 95% of the way there - and it's working great now! I'll edit my answer above to incorporate some of these things. Let me know if you have any more questions, happy to help. – smileyborg
10 upvote
  flag
If I was 95% the way there, the remaining 5% would have taken me half a week to figure out! I really appreciate you throwing in and lending a hand so quickly. Working code is up on GitHub so and the rest of the community can benefit. Really great work smileyborg! – caoimghgin
upvote
  flag
@smileyborg Are you using this method for long (perhaps infinite, e.g. twitter) lists? Curious how bad the scrolling impact is. – Bob Spryn
upvote
  flag
Yeah I used the term loosely, more to mean infinite loading style lists a la twitter. But that's what I was wondering. You are technically rendering the cell twice now for each cell that's shown. Too bad you can't get a reference to the true cell in heightForRow. That would be optimal. – Bob Spryn
upvote
  flag
Yep I follow. True, the view doesn't get added to the screen so you don't technically render it. Working like a charm so far. – Bob Spryn
upvote
  flag
@stevex did you set translatesAutoresizingMaskIntoConstraints=NO in the UIWindow and the self.view? – Jano
upvote
  flag
Ok, I modified the project provided above to illustrate my issue. github.com/Alex311/TableCellWithAutoLayout – Alex311
4 upvote
  flag
@Alex311 Very interesting, thanks for providing this example. I did a little testing on my end and wrote up some comments here: github.com/Alex311/TableCellWithAutoLayout/commit/… – smileyborg
upvote
  flag
smiley, check this out ! //allinonescript.com/questions/20238559/… – Fattie
upvote
  flag
@Alex311 :: I am getting "ambigious layout" warning while running sample at github.com/Alex311/TableCellWithAutoLayout – muzz
upvote
  flag
One hiccup I ran into here is that you need to make sure your constraints (created in IB) are referencing the contentView and not the cell itself. If you created it in a version of IB prior to Xcode 5, you are likely referencing the cell, in which case evaluating just the contentView for height won't work. – Bob Spryn
upvote
  flag
@BobSpryn This is the same issue that wildmonkey raises his answer to this question. I strongly recommend creating and managing all your constraints in code to avoid these sorts of issues, as you'll know exactly which constraints are being created and where they are going. I also recommend using an API to make your life much easier; here is the one I design, maintain, and use exclusively: github.com/smileyborg/UIView-AutoLayout – smileyborg
upvote
  flag
Not as necessary anymore with Xcode 5. In fact I get lots of mileage out of IB now for that, and only need to do complicated setups in code. – Bob Spryn
upvote
  flag
@smileyborg Great answer. However I'm facing some problems with my scenario //allinonescript.com/questions/20597968/… . I'd glad to hear what do you think about it. – Fred Collins
upvote
  flag
@smileyborg you don't set the cell's labels' preferredMaxLayoutWidth in tableView:cellForRowAtIndexPath:. I think the reason your solution works as it's right now is because you're dequeuing a cell in tableView:cellForRowAtIndexPath: that was previously dequeued in tableView:heightForRowAtIndexPath: and had its labels' preferredMaxLayoutWidth set there. – Tom Abraham
upvote
  flag
@TomAbraham You are totally right; I edited the answer to fix it! By the way, another solution to this is to override layoutSubviews in the cell itself, set the preferredMaxLayoutWidth on all multi-line labels after calling super, and then call [super layoutSubviews] again. That removes the need for the view controller to set it. – smileyborg
3 upvote
  flag
I would STRONGLY recommend caching the cell for each cell reuse identifier type. Each time you dequeue for height you are taking a cell out of the queue that is not being added back. Caching can significantly lower the amount of times the initializer for your table cells is called. For some reason, when I was not caching, the amount of memory being used kept growing as I would scroll. – Alex311
upvote
  flag
The github.com/samsymons/RedditKit example also has an invisible cell as a subview of the table being used as a cached cell during auto layout calculation to avoid doing dequeues in the heightForRowAtIndexPath method – Bjergsen
upvote
  flag
In regards to step 1, constraints on subviews don't seem to exert an influence on the size of container views laid out by the system (e.g., self.view, self.tableView.tableHeaderView, table cells, etc.). – bilobatum
upvote
  flag
@bilobatum If your subviews have an intrinsic content size and they are rigidly connected to their container, they most definitely will have an impact. Remember though, the intrinsic constraints that exert this "pressure" are lower than Required priority, so if you have other constraints that are of higher priority, you won't see this effect. If you have a specific issue you're running into (or want more clarification), feel free to post more details or a sample project/code. – smileyborg
upvote
  flag
Any hint on performing this with a section index. I manage to have the autolayout working and displaying as long as I don't have a section. With section index it fails due to me to cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds)); which takes into account the tableview width and not the remaining place available for the cell once a section index is present. – HpTerm
upvote
  flag
@HpTerm The other post I linked above has all the info you need. It looks like you will need some amount of hardcoding. – smileyborg
upvote
  flag
@smileyborg I already knew about the other post you gave, but I was looking for "new/fresh" information. That post hardcodes things which I already done but I am looking for a cleaner way. Thanks anyway. – HpTerm
upvote
  flag
This almost works for me except when rotating to landscape mode: the size is calculated as if the phone was in portrait mode, so the labels will be often bigger than necessary. Has anyone encountered this issue? I set the set the cell.bounds property, and it has the right value, but it does not seem to have any effect. – gyim
upvote
  flag
@gyim Is the cell's contentView the correct size in landscape during tableView:heightForRowAtIndexPath:? If you could post a sample project on GitHub demonstrating the issue that would help. – smileyborg
upvote
  flag
I believe the recommended way for detecting height is to have a 'dummy' off-screen cell, rather than reading the cell you intend to update directly. – Ash
upvote
  flag
@smileyborg I am having an issue displaying custom cell with dynamic contents. Can you please look at my question //allinonescript.com/questions/21457871/…? – Geek
upvote
  flag
Instead of doubling my cell usage by dequeuing cells in the heightForRowAtIndexPath: delegate method, I instantiated a dedicated cell in my view controller's viewDidLoad implementation for the height calculations. – Kiel Gillard
upvote
  flag
@KielGillard That's a perfectly valid way to do it, if you read the comments in the code samples above that's also one of my suggestions. The trick if you do it that way is to make sure you keep one offscreen cell per reuse identifier. – smileyborg
upvote
  flag
@smileyborg what are my options if the number of different layouts is more than 4?(in my case all the different combinations of the layout is currently 24) – kernix
upvote
  flag
@kernix You'll need 24 reuse identifiers. You could consider generating the strings programmatically. But perhaps consider seeing if you can't eliminate some unique layouts. – smileyborg
upvote
  flag
Settings 24 unique layouts means a lot of "if" statements :) I'll try to decrease the amount of layouts to the minimum needed by chaning height constraints to 0 for hiding(with multiline labels I hope that setting its text to an empty string will cause the height to be zero). I'll update later. Thanks! – kernix
upvote
  flag
@smileyborg I'm now able to use UICollectionView; it seems that the problem was due to using CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].h‌​eight; Instead I didn't use contentView but directly on the cell itself: [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].h‌​eight; and now it works. – kernix
upvote
  flag
@smileyborg very cool, I'll update you once I upload a sample project to github. – kernix
upvote
  flag
@Alex311 I finally was able to dive into this and verify what you were seeing. Running under Instruments confirms that table view cells dequeued but never returned from tableView:cellForRowAtIndexPath: are leaked. So I've updated everything to officially recommend avoiding dequeue during height calculations, and instead rely on offscreen cells held by the view controller. See some additional discussion and a link to the sample project commit that resolves this issue here: github.com/caoimghgin/TableViewCellWithAutoLayout/issues/16 – smileyborg
upvote
  flag
I have a table view with ~20 rows, with about 10 different unique layouts. Is there still any real benefit in such a case to have the separate off screen cell for height calculation, rather than dequeue one cell, populate it, and both use it in heightForRowAtIndexPath and return it in cellForRowAtIndexPath? – sgvd
upvote
  flag
@sgvd Not really - if you will never have more than a couple dozen rows, you can definitely do what you're suggesting. (The only reason this doesn't scale is because you'll end up holding a cell in RAM for every cell in the table. But that's totally fine for small table views.) – smileyborg
upvote
  flag
Actually trying that, having still a few cells with the same identifier I did have an issue with cells being recycled and dequeued for a different row again, so my cache was invalid. This especially happened when using estimatedHeightForRowAtIndexPath, without I didn't see it, but not sure if that was just luck. So now using separate off-screen cells afterall too. – sgvd
upvote
  flag
@sgvd If you cache the cells by index path, never call dequeue..., and clear the cache when appropriate, this will work. The table view will never recycle cells if you don't call dequeue.... I have some code doing exactly this in a production app and it works fine. :) – smileyborg
upvote
  flag
@smileyborg Isn't [cell setNeedsLayout]; [cell layoutIfNeeded]; redundant? Can't you just call the second, or is there something going on here that I'm missing? [cell layoutIfNeeded]; docs say that it forces layout. – Paul Solt
upvote
  flag
@PaulSolt setNeedsLayout effectively sets an internal BOOL flag on the view that it needs a layout pass. As the name suggests, layoutIfNeeded won't do anything unless that flag is YES. So, in order to be absolutely sure that we force a layout pass, setNeedsLayout is called first. This is probably redundant because the internal needs layout flag is likely to be YES already, but for the sake of robustness it doesn't hurt to make sure. (Another reason setNeedsLayout exists is when you don't need an immediate layout calculation, iOS will do the layout pass for you for views that need it.) – smileyborg
upvote
  flag
@smileyborg: thanks for your excellent answer. I'm trying to translate it into C#/Xamarin but my cells always measure as zero. Any chance you could take a look at this to see if there's anything obvious I'm doing wrong? – Kent Boogaart
upvote
  flag
@KentBoogaart Sorry but I don't use Xamarin, I do all iOS development in native Objective-C using Apple's SDK. (Xamarin makes it potentially much more difficult to debug because you have yet another layer in which things can break.) – smileyborg
upvote
  flag
@smileyborg: yeah, I realize this is outside your area of expertise. Guess I just hoped you could look for anything obvious - it's pretty easy to read C# if you know Objective-C. Anyways, I'll try going over it again. – Kent Boogaart
upvote
  flag
@KentBoogaart It's not about reading the C# (that's not an issue), it's about needing to get the full picture which includes inspecting the Auto Layout constraints on the cell, as well as potentially understanding what is involved when Xamarin translates the C# method calls to the underlying Cocoa Touch SDK. In any case, if you do get it working it might be helpful for other folks to have a similar Xamarin-based sample project to refer to. – smileyborg
upvote
  flag
@smileyborg: I have figured out how to get this working in C# and will throw it up on Github soon. One weird thing that threw me is that my insets were larger and this was causing unsatisfiable constraints. If I change your project such that kLabelVerticalInsets is 20 instead of 10, I see the same thing when I run your sample. Any idea why this is? – Kent Boogaart
upvote
  flag
@KentBoogaart Sweet! As far as the exceptions you're seeing, check out the comment on Line 77 of RJTableViewCell.m in my sample project. You'll see a brief discussion of why this is happening (it's not a big deal), and if you uncomment Line 80 you'll see it's fixed. – smileyborg
upvote
  flag
@smileyborg: ah, I see - thanks. Here is my git repo: github.com/kentcb/TableViewCellWithAutoLayout_dotNET – Kent Boogaart
upvote
  flag
@KentBoogaart Nice, I added the link to the Xamarin sample project into the answer itself. – smileyborg
86 upvote
  flag
Wow, I cannot believe how complicated this is. Thanks for such a great post. – Nathan Buggia
upvote
  flag
@AlfieHanssen Have you tried the sample project? That includes a + button to insert rows with animation. I haven't seen any major issues there -- could you perhaps fork that project and modify it to demonstrate the issue(s) you're seeing? – smileyborg
upvote
  flag
@smileyborg, you are right, the insert looks very good in the sample project. I'm working on a collapsable / expandable tableview where i'm inserting sections and rows. Maybe that's part of the issue. I may or may not get around to forking and replicating. Either way, thanks for responding! – Alfie Hanssen
upvote
  flag
@smileyborg! Fixed it! All I had to do was comment out self.tableview.estimatedRowHeight = 70.0f and the crash on insertion went away, and the insertion animations are smooth as hell! This answer was relevant. – Alfie Hanssen
upvote
  flag
@AlfieHanssen So you're saying that it's not reproducible when you aren't using row height estimation? That's not really a fix -- it does suggest that the problem may be Apple's bug though. But row height estimation is critical for performance for large table views with variable row heights, especially when Auto Layout is being used. Unless you have a very small number of rows, this won't work. – smileyborg
upvote
  flag
This didn't work for me, because cells created in Interface Builder aren't loaded when using alloc init. The version prior to the Mar 11 edit works. I think I'll dequeue and cache to limit the leak. – nschum
upvote
  flag
@nschum Yet another reason to skip IB altogether :) But if you are committed to using nibs, all you need to do is load the nib with your custom table view cell instead, see here for how to do that: //allinonescript.com/a/1939305/796419 (You don't want these offscreen template cells added into the table view reuse pool, so don't worry about getting that hooked up.) – smileyborg
1 upvote
  flag
Storyboards, actually. I don't think that works for prototype cells (at least without re-instantiating the whole VC). It might be possible to avoid the leak altogether by dequeuing in the heightForRowAtIndexPath, keeping the cell and returning it the next time cellForRowAtIndexPath is called. – nschum
4 upvote
  flag
seriously.... Apple couldn't just simply allow cell.rowHeight = DYNAMIC_HEIGHT; return cell; ????? WHAT WERE THEY THINKING??? – user1504605
upvote
  flag
As per link added in answer & description on github this approach works with iOS 7+. Does it work on iOS 6 as well ? If not , what changes I need to make in order to work on iOS 6 as well ? Thanks – iOSAppDev
upvote
  flag
@iOSAppDev iOS 6 doesn't include the estimatedRowHeight facility, which makes this approach effectively unviable on iOS 6 because every cell's height will need to be calculated up front. But other than that, everything else should be basically the same. In any case, now is a good time to stop developing for iOS 6 - the vast majority (~95%) of users are already on iOS 7, and that number is only going to increase going forward into iOS 8. – smileyborg
upvote
  flag
@smileyborg Thanks for your reply. Actually client wants to support iOS 6 . Can you suggest some alternate for how can I achieve this for iOS 6 as well ? I tried with updating the height constraint of label at runtime using text length but its not smooth even after caching the height of cell. When cells are loaded for first time using web service response , due to height calculation animation is not smooth – iOSAppDev
upvote
  flag
@iOSAppDev Only option is to not use auto layout at all inside table view cells. Performance is going to be a major issue otherwise. – smileyborg
upvote
  flag
I don't think you need self.tableView.rowHeight = UITableViewAutomaticDimension. – ma11hew28
upvote
  flag
@MattDiPasquale Check out WWDC 2014 session 226 (What's New in Table and Collection Views). Luke specifically says that the rowHeight property must be set to UITableViewAutomaticDimension for the cell self sizing to work...as of iOS 8 this value is normally the default, but in the initial iOS 8 beta there was a bug with table views instantiated from a nib/Storyboard causing you to need to manually set this value programmatically. In the end, it doesn't hurt to set this explicitly since it's a potential source of subtle issues otherwise. – smileyborg
upvote
  flag
@smileyborg Oh, thanks. That makes sense. I'm creating the table view programmatically, not from a nib/storyboard. – ma11hew28
upvote
  flag
I understand that this requires that you also create all subviews of YourTableViewCellClass and constraints between programmatically and not in IB, right? – Drux
upvote
  flag
@Drux No, you can use IB if you really want to for pretty much anything. It is more difficult to use in many cases, however. – smileyborg
upvote
  flag
@smileyborg But then you would have to change from [[YourTableViewCellClass alloc] init]to some sort of dequeue-and-remove-cell-from-view-hierarchy in the sample code, right? Your recommendation is instead for setting up the offscreen cell plus its subviews and constraints programmatically, right? And +1 BTW :) – Drux
1 upvote
  flag
@Drux The preferred method to get a new instance of a cell from IB is to load the nib directly. You can dequeue a cell and rely on that to instantiate one for you (as of iOS 5), but note that if you do not return the cell from cellForRowAtIndexPath it may leak due to an Apple bug. And yes I recommend doing everything in code: you know exactly what is happening, everything is in one place, you can use constants and variables and do runtime config that can't be done in IB (compile time), & more. – smileyborg
1 upvote
  flag
@Drux Yes, although the height also depends on the correct width because that is what determines when the multi-line body label word wraps. If you have further questions, let's continue the discussion in chat: chat.//allinonescript.com/rooms/info/57099/… – smileyborg
upvote
  flag
Have you found any issues with this approach for custom section headers? – Chet
upvote
  flag
@Chet Nope, in fact it's simpler since you're just going to set up your custom UIView, force a layout pass, and then call systemLayoutSizeFittingSize: on it. Just be wary that if you dequeue header views for the purpose of calculating their height alone, they may leak due to the same bug that causes cells to leak. – smileyborg
upvote
  flag
Well I'm using UITableViewCells for the headers. Any I'm having issues. :/ I'll try what you just said though. Thanks – Chet
upvote
  flag
@Chet Oh, I definitely don't think you are supposed to use UITableViewCell for headers & footers. Just use a regular UIView subclass. – smileyborg
upvote
  flag
I see. And then I shouldn't deque them either? – Chet
upvote
  flag
@Chet Not unless you have tons of section headers and need to for performance reasons. And if you do, only do so when you actually return the view, not in the heightForHeader/Footer callbacks. – smileyborg
upvote
  flag
@smileyborg How would this change if the images were downloaded asynchronously, and the height of the row depends on them? – Christopher Francisco
upvote
  flag
@ChristopherFrancisco You probably want to display a loading indicator while your images fetch, then call reloadData on the table view once you have your data. You should also be able to use reloadRowsAtIndexPaths if you want to display a placeholder cell until each loads. Basically, it shouldn't be much different from what you'd do if you weren't using auto layout. – smileyborg
upvote
  flag
@smileyborg I'm using SDWebImage to load the images async. Since the height of the cells depend on the image height, once the image has been downloaded I guess I call reloadRowsAtIndexPaths. Then I must calculate the row height using previously downloaded image in heightForRowAtIndexPath. But how do I do that? My constraints are set inside the prototype cell in the storyboard – Christopher Francisco
upvote
  flag
@ChristopherFrancisco Once you have the image in the image view, you don't do any work yourself...Auto Layout will correctly size the image assuming your constraints are right, which will cause the cell's height to calculate correctly. – smileyborg
upvote
  flag
Hi, @smileyborg, I noticed that you Call [super updateConstraints] as the first step in RJTableViewCell, but the document says "Call [super updateConstraints] as the final step in your implementation.", Can you explain why do this? Thank you. – Vincent
upvote
  flag
@Vincent I haven't noticed any difference in regards to where the call to [super updateConstraints] is placed. There's no specific reason why that call to super is at the top of the method, I think it's just been that way since the code was originally written. If you can find any functional difference please do share, and I'm fine updating the code either way. – smileyborg
upvote
  flag
From objc.io : "The first step - updating constraints - can be considered a" measurement pass "It happens bottom-up (from subview to super view)" and official documents also say so. But honestly, I did not find them any different. Might be my personal reasons , are more willing to comply with the official documents. Also, thank you for your PureLayout, it is great, I've been using. – Vincent
upvote
  flag
Still not working on ios8 :( The layout has this constraint, which should not happen: <NSAutoresizingMaskLayoutConstraint:0x1589d3c0 h=--& v=--& V:[UITableViewCellContentView:0x146c99d0(43.5)]>", – Softlion
upvote
  flag
@Softlion That constraint is actually expected, and corresponds to the fixed height of the table view cell and its content view. If you're getting a constraint exception with that in it, it means that your cell height (44 pt cell height - 0.5 pt for cell separator = 43.5 pt content view height) isn't correctly sized for the constraints and subviews in your cell. Go back to step 1 :) Be sure to take a look at the sample projects to see everything working correctly. – smileyborg
upvote
  flag
ok for the separator. 44 is my estimated cell height. Constraints are correclty set and works ok on ios7. I only removed the code which computes the height for each cell using a prototype cell as advertised for ios8 and set the estimation on the table view. Only difference, your sample code uses a UITableViewController, but i don't. – Softlion
upvote
  flag
Should the app be compiled with the ios8 sdk to use the new ios8 mode ? – Softlion
1 upvote
  flag
@Softlion Yes, you must be compiling with the iOS 8 SDK to use the self-sizing cell mechanism. Also make sure that the table view's row height is set to UITableViewAutomaticDimension. – smileyborg
upvote
  flag
That's it. You have to compile against the iOS 8 SDK. Otherwise it does nothing. – Softlion
upvote
  flag
Has anyone noticed a stutter while scrolling when heightForRow actually fires? When I use estimatedHeightForRow it makes reloading the table much faster, but there is a noticeable jitter when cells display for the first time (before I cache the height). – Stakenborg
upvote
  flag
@Stakenborg If you're using a slower device (iPhone 4/4S, iPad 2) and your cell is significantly complex (more than a few subviews and many constraints), this definitely happens due to the fact that the Auto Layout engine runs its calculations on the main thread. If the auto layout calculation for a cell takes more than ~16ms, frames will be dropped causing stuttering in the UI. Since you are caching heights, this only happens the first time for each cell type. Unfortunately there isn't any way to prevent this issue with the current APIs, aside from using simpler cells or faster devices. – smileyborg
upvote
  flag
@smileyborg Thanks for the followup! Unfortunately, my cells all contain varying heights due to dynamic label sizes. Trying to see if I can implement some sorcery so I don't have to fire an auto layout calculation after I have the base cell height cached without making my code look like mud. – Stakenborg
upvote
  flag
@smileyborg Thanks for the detailed explanation. Just wanted to confirm one thing: the iOS 8 self-sizing technique doesn't work with iOS 7 devices even when compiling with the iOS 8 SDK (and setting the deployment target to 7). Right? – Manav
1 upvote
  flag
@Manav That's correct, iOS 7 doesn't support self sizing cells...you should be able to easily test this to confirm. – smileyborg
upvote
  flag
@smileyborg Yes, I'd confirmed this by testing. Was just double checking to ensure that I wasn't doing anything stupid. Thanks! – Manav
upvote
  flag
@smileyborg - great post! BTW, the use of dequeueReusableCellWithIdentifier in your code above, can return nil. Someone will get bit by this. – drudru
1 upvote
  flag
I think the idea of using different reuseIds for seemingly similar cells / classes is a great takeaway here. It helps keeps constraints in check and minimizes issues related to resizing. – Brian Sachetta
upvote
  flag
Struggled to implement dynamic height in my iOS 8 project. It was missing tableView.estimatedRowHeight and it just would not work without it – EhTd
upvote
  flag
@smileyborg what if we have to support both iOS 7 and 8, do we need to do runtime check ? – onmyway133
upvote
  flag
@onmyway133 The iOS 7 compatible implementation works fine on iOS 8. It's just more work. – smileyborg
upvote
  flag
@smileyborg your solution is working well, except I can't seem to get it to work when there a relative constraints involved. See separate question here: //allinonescript.com/q/27064070/368085 – mluisbrown
upvote
  flag
@smileyborg checked out your sample project for iOS 8. It works, but after navigating to another view controller (navigationController?.pushViewController(UIViewController()‌​, animated: true)) and back it breaks - scrolling becomes jumpy. Any ideas about the cause of this? I have Xcode 6.1.1, the issue is reproduced on iPhone 6 with iOS 8.1.3 – Andrii Chernenko
upvote
  flag
@deville This sounds like the same issue described here (which I don't have any good workaround for): github.com/smileyborg/TableViewCellWithAutoLayoutiOS8/issues‌​/17 – smileyborg
upvote
  flag
With regards to this issue I also found out that when the table view is reloaded, the jumping issue occurs when scrolling up. I just added a tableView.reloadData() when the cell is tapped, also commented the tableView.allowsSelection = false method. I guess we will have to wait for Apple to fix this then. – otakuProgrammer
upvote
  flag
Man, recreating the cell in just to get the height for heightForRowAtIndexPath: just feels bad :( – Hlung
1 upvote
  flag
This worked EXCEPT the width of my cell wasn't filling the full table view width. I fixed by adding a blank UIView to my cell with constraints trailing and leading set. Then during heightForRowAtIndexPath, programmatically add a width constraint set to the root controller's view bounds.width (or change the constraint.constant for any second call.) – alpsystems.com
upvote
  flag
On heightForRowAtIndexPath my cell does not get its real height value. It stays fixed at 44 (the default for the tableView). What could be the reason ? – Petar
upvote
  flag
height += 1.0f; should be height + 1.0/[UIScreen mainScreen].scale; – Uladzimir
upvote
  flag
@ChristopherFrancisco, were you able to solve the problem with async image loading + autolayout? I have the same issue and not sure how to start. The image heights vary, so I don't know what the heights are in advance. My autolayout + autosizing is working for labels, but not for images. – Dean
upvote
  flag
Thank you for this great post. The iOS 8 approach works great for me until I start inserting sections to the bottom of the table view while the user is scrolling. And I'm 99% sure it's UITableViewAutomaticDimension to blame, so I'm back to using the "iOS 7 approach". May be you have some tips on how to solve this using UITableViewAutomaticDimension ? – dariaa
upvote
  flag
When loading data from web (asynchronous) where and how should I notify to table that the datasource changed? – Nicolas Durán
upvote
  flag
Can someone help me with this problem? This solution works just fine but if I have an UIImageView with AspectRatio constraint, the constraint will not work and log printed out Unsatisfied constraint. – kientux
upvote
  flag
Absolutely awesome! - now that I only support iOS8 and upwards; this is a hell of a lot simpler now! – Tander
upvote
  flag
Thanks @smileyborg. Does anyone known if I have to add constraints (pin) between the contentView and the superview (which is the cell itself)?. I don't se anything about that. Are those constraints added automatically by the system? Note that I'm adding contraints using code rather than interface builder. Thanks a lot. – Ricardo
1 upvote
  flag
@Ricardo You don't need to (and shouldn't) add constraints between the cell and its contentView. The reason is because both of those views are created and managed by the system (UIKit); you just need to worry about what you put inside the contentView. – smileyborg
upvote
  flag
Thanks @smileyborg for reply. Do you remember if there is any reference about that in apple docs or any wwdc video? I only saw in the first part of this video developer.apple.com/videos/play/wwdc2014-226 how they use constraints programatically as you said, using only the contentView, but nothing about the cell itself. Thanks in advance. – Ricardo
1 upvote
  flag
@smileyborg we followed this scheme in iOS 8 and it worked beautifully, but has broken in iOS 9 only some of the time. Might there be changes to the way dynamic cell heights are handled in iOS 9? Thanks so much for the descriptive answer. – loudmouth
upvote
  flag
You don't have to implement -heightForRowAtIndexPath on iOS8+. Check captechconsulting.com/blogs/…. – Raphael Oliveira
upvote
  flag
Hi! I'm using the technique used for iOS 7 on iOS 9 on the simulator cause I want to have compatibility. That said, the system clips my uilabel's content sometimes when I use an accessory view. Can it be? – no_ripcord
upvote
  flag
if you're having problems with the constraint, one thing that helped me: if you have two views side by side, don't pin one to the top and center the other's Y to the one pinned to the top, that won't work. Pin both to the top. – no_ripcord
2 upvote
  flag
@NathanBuggia, Ohh Gosh!! Dear Apple, was it so hard to just put wrap_content anywhere there huh? – guness
upvote
  flag
The iOS 7+ method doesn't account for the current traitCollection. So if you have a cell that has a different layout for compact size class then the height calculated won't be right. – trapper
2 upvote
  flag
Is the iOS8 implementation still recommended for iOS9 and iOS10, or are there new approaches since this answer was published? – Koen
upvote
  flag
I can't get height calculated dynamically inside uitableviewcell's uitableview – Anil Kukadeja
upvote
  flag
For the dynamic auto-layout image view base on SDWebImage, could someone help me to check this issue? – Itachi
upvote
  flag
you are the man. thanks. – boraseoksoon

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.

upvote
  flag
systemLayoutSizeFittingSize needs to be called on contentView, not cell – onmyway133

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).

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

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.

upvote
  flag
This is working for me on iOS7, is it now OK to call tableView:cellForRowAtIndexPath: in tableView:heightForRowAtIndexPath: now? – Ants
upvote
  flag
Ok so this is not working, but when I call systemLayoutSizeFittingSize: in tableView:cellForRowAtIndexPath: and cache the result then and then use that in tableView:heightForRowAtIndexPath: it works well as long as the constraints are setup correctly of course! – Ants
upvote
  flag
In iOS 7 it works. How to make it work in iOS 8? – Winston
upvote
  flag
This only works if you use dequeueReusableCellWithIdentifier: instead of dequeueReusableCellWithIdentifier:forIndexPath: – Antoine
1 upvote
  flag
I really don't think calling tableView:cellForRowAtIndexPath: directly is a good way. – Itachi

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 //allinonescript.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!

2 upvote
  flag
You should just be able to simply use a: [YourCell new] and use that as the dummy. As long as the constraint code building code is fired in your instance, and you trigger a layout pass programmatically you should be good to go. – Adam Waite
1 upvote
  flag
Thanks! This works. Your category is great. It is what made me realize that this technique works with UICollectionViews as well. – Ricardo Sanchez-Saez
2 upvote
  flag
How would you do this using a prototype cell defined in a storyboard? – David Potter
upvote
  flag
Pass! I've not thought about that. – Adam Waite

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.

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;
}
1 upvote
  flag
This has already been extensively covered in the accepted answer to this question. – smileyborg
2 upvote
  flag
Yes, I know... but I didn't want to use PureLayout and the dispatch_once 'trick' helped me a lot, to work it out only using the Storyboard. – Mohnasm
1 upvote
  flag
Also, this is a nice clean concise answer. – Leslie Godwin

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.

- (void)viewWillAppear:(BOOL)animated {
    _messageField.delegate = self;
    _tableView.estimatedRowHeight = 65.0;
    _tableView.rowHeight = UITableViewAutomaticDimension;
}
2 upvote
  flag
Setting up the autolayout correctly, together with this code being added in viewDidLoad did the trick. – Bruce
upvote
  flag
but what if estimatedRowHeight is varying row by row? should I over or under estimate? use minimum or maximum of the height I use in tableView? – János
1 upvote
  flag
@János this is the point of rowHeight. To do this behave as expected you need to use constrains without margins and align to the TableViewCell the objects. and I asume you are using a UITextView so still you need to remove autoscroll=false otherwise it will maintain the height and Relative height wont act as expected. – eddwinpaz
1 upvote
  flag
That is by far the most robust solution. Worry not about estimatedRowHeight, it mostly affects the size of the scroll bar, never the height of actual cells. Be bold in the height you pick: it will affect insertion/deletion animation. – SwiftArchitect
upvote
  flag
Here's the sample code in swift 2.3 github.com/dpakthakur/DynamicCellHeight – Deepak Thakur

I've also found a YouTube video explaining how to achieve this using the STV framework.

upvote
  flag
Hey Matt, nice example but I still can't figure how to do it. And in case for example if I have to labels with different height every time. And what is the task entity object in storyboard? – Matrosov Alexander

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.

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.

5) 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. :)

For IOS8 it's really simple:

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableViewAutomaticDimension
}

OR

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

But for IOS7, 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.

6 upvote
  flag
I think we don't need to implement the heightForRowAtIndexPath function in the OS8 case – jpulikkottil
upvote
  flag
As @eddwinpaz notes on another answer, it is important that the constraints used to lock in the row height do NOT include margin. – Richard
1 upvote
  flag
+1 for "If multiple lines labels, don't forget set the numberOfLines to 0" this was causing my cells to not be dynamic in size. – stackoverflow_user01

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.

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
    }
}
upvote
  flag
note: systemLayoutSizeFittingSize not work in my case – djdance
upvote
  flag
cell height is not correct in cellForRowAtIndexPath, the cell is not laid out yet at this point. – Andrii Chernenko
upvote
  flag
in iOs7 it is fixed value, i.e. it works. You can set in outside cellForRowAtIndexPath if you want – djdance

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

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?

(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.

1 upvote
  flag
Wow thank you!! I didn't think Apple would be this stupid after 2 OS versions with auto sizing cells!! But turns out we need to wait ios15 for this to work correctly. Anyway you saved me trouble from hacking the hugging properties of cell and other views etc. – EralpB
upvote
  flag
hi is it possible to give dynamic height for cell, that having tableview with cell with dynamic cell content.? – Jignesh B

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.

tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension

enter image description here

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

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

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 dataSource and delegate
  • Assign UITableViewAutomaticDimension to rowHeight & estimatedRowHeight
  • Implement delegate/dataSource methods (i.e. heightForRowAt and return a value UITableViewAutomaticDimension to it)

-

@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
    table.rowHeight = UITableViewAutomaticDimension
    table.estimatedRowHeight = UITableViewAutomaticDimension
}



// UITableViewAutomaticDimension calculates height of label contents/text
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    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

upvote
  flag
Thank you seems like a clear and simple solution but I have a problem I am not using labels but textviews so I need the row height to increase as data is added. My problem then is to pass info to heightForRowAt. I can measure the height of my changing textviews but now need to change the row height. Would appreciate some help please – Jeremy Andrews
upvote
  flag
@JeremyAndrews Sure will help you. Raise your question with source code you've tried and detail about problem. – Krunal

Have tried:

func textViewDidChange(_ textView: UITextView) {
    var frame = textView.frame
    frame.size.height = textView.contentSize.height
    textView.frame = frame
    print(frame)
    self.tableView.rowHeight = frame.size.height

but although I get the right frame height the row does not adjust

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

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

In general, as long as with apple autolayout can reach the height of the basic adaptive.

OC + Swift double version for dynamic list UITableView data loading, and the cell dynamic highly adaptive. look at:https://github.com/ReverseScale/AutoCellHeightDemo

Or,Template auto layout cell for automatically UITableViewCell height calculating. Github:https://github.com/ReverseScale/UITableView-FDTemplateLayoutCell

I hope it can help you!

Not the answer you're looking for? Browse other questions tagged or ask your own question.