How to Animate Uitableviewcell Height Using Auto-Layout

How to animate UITableViewCell height using auto-layout?

The following worked for me:

Preparation:

  1. On viewDidLoad tell the table view to use self-sizing cells:

    tableView.rowHeight = UITableViewAutomaticDimension;
    tableView.estimatedRowHeight = 44; // Some average height of your cells
  2. Add the constraints as you normally would, but add them to the cell's contentView!

Animate height changes:

Say you change a constraint's constant:

myConstraint.constant = newValue;

...or you add/remove constraints.

To animate this change, proceed as follows:

  1. Tell the contentView to animate the change:

    [UIView animateWithDuration: 0.3 animations: ^{ [cell.contentView layoutIfNeeded] }]; // Or self.contentView if you're doing this from your own cell subclass
  2. Then tell the table view to react to the height change with an animation

    [tableView beginUpdates];
    [tableView endUpdates];

The duration of 0.3 on the first step is what seems to be the duration UITableView uses for its animations when calling begin/endUpdates.

Bonus - change height without animation and without reloading the entire table:

If you want to do the same thing as above, but without an animation, then do this instead:

[cell.contentView layoutIfNeeded];
[UIView setAnimationsEnabled: FALSE];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled: TRUE];

Summary:

// Height changing code here:
// ...

if (animate) {
[UIView animateWithDuration: 0.3 animations: ^{ [cell.contentView layoutIfNeeded]; }];
[tableView beginUpdates];
[tableView endUpdates];
}
else {
[cell.contentView layoutIfNeeded];
[UIView setAnimationsEnabled: FALSE];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled: TRUE];
}

You can check out my implementation of a cell that expands when the user selects it here (pure Swift & pure autolayout - truly the way of the future).

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

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

  • iOS 8 Sample Project - Requires iOS 8
  • iOS 7 Sample Project - Works on iOS 7+

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 can be just as easy as and significantly more powerful than Interface Builder using layout anchors, or one of the fantastic open source APIs available on GitHub.

  • 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 UITableView.automaticDimension. 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 = UITableView.automaticDimension;
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 on screen, 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 on screen, 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 Autolayout 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.0, 0.0, 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 multiline 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.0;

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.0;
} else {
return 40.0;
}
}

Sample Projects

  • iOS 8 Sample Project - Requires iOS 8
  • iOS 7 Sample Project - Works on iOS 7+

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

Xamarin (C#/.NET)

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

UITableViewCell animate height issue in iOS 10

Better Solution:

The issue is when the UITableView changes the height of a cell, most likely from -beginUpdates and -endUpdates. Prior to iOS 10 an animation on the cell would take place for both the size and the origin. Now, in iOS 10 GM, the cell will immediately change to the new height and then will animate to the correct offset.

The solution is pretty simple with constraints. Create a guide constraint which will update it's height and have the other views which need to be constrained to the bottom of the cell, now constrained to this guide.

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
UIView *heightGuide = [[UIView alloc] init];
heightGuide.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:heightGuide];
[heightGuide addConstraint:({
self.heightGuideConstraint = [NSLayoutConstraint constraintWithItem:heightGuide attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:0.0f];
})];
[self.contentView addConstraint:({
[NSLayoutConstraint constraintWithItem:heightGuide attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
})];

UIView *anotherView = [[UIView alloc] init];
anotherView.translatesAutoresizingMaskIntoConstraints = NO;
anotherView.backgroundColor = [UIColor redColor];
[self.contentView addSubview:anotherView];
[anotherView addConstraint:({
[NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:20.0f];
})];
[self.contentView addConstraint:({
[NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeLeft multiplier:1.0f constant:0.0f];
})];
[self.contentView addConstraint:({
// This is our constraint that used to be attached to self.contentView
[NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:heightGuide attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
})];
[self.contentView addConstraint:({
[NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeRight multiplier:1.0f constant:0.0f];
})];
}
return self;
}

Then update the guides height when needed.

- (void)setFrame:(CGRect)frame {
[super setFrame:frame];

if (self.window) {
[UIView animateWithDuration:0.3 animations:^{
self.heightGuideConstraint.constant = frame.size.height;
[self.contentView layoutIfNeeded];
}];

} else {
self.heightGuideConstraint.constant = frame.size.height;
}
}

Note that putting the update guide in -setFrame: might not be the best place. As of now I have only built this demo code to create a solution. Once I finish updating my code with the final solution, if I find a better place to put it I will edit.

Original Answer:

With the iOS 10 beta nearing completion, hopefully this issue will be resolved in the next release. There's also an open bug report.

  • https://openradar.appspot.com/27679031
  • https://forums.developer.apple.com/thread/53602

My solution involves dropping to the layer's CAAnimation. This will detect the change in height and automatically animate, just like using a CALayer unlinked to a UIView.

The first step is to adjust what happens when the layer detects a change. This code has to be in the subclass of your view. That view has to be a subview of your tableViewCell.contentView. In the code, we check if the view's layer's actions property has the key of our animation. If not just call super.

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
return [layer.actions objectForKey:event] ?: [super actionForLayer:layer forKey:event];
}

Next you want to add the animation to the actions property. You might find this code is best applied after the view is on the window and laid out. Applying it beforehand might lead to an animation as the view moves to the window.

- (void)didMoveToWindow {
[super didMoveToWindow];

if (self.window) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"bounds"];
self.layer.actions = @{animation.keyPath:animation};
}
}

And that's it! No need to apply an animation.duration since the table view's -beginUpdates and -endUpdates overrides it. In general if you use this trick as a hassle-free way of applying animations, you will want to add an animation.duration and maybe also an animation.timingFunction.

Animate Height of UITableView

The issue is with autolayout. You have to also set the height constraint of the tableview in order to keep the tableview at the new height/origin.

For anyone else who may have this issue:

Dynamic UITableView height

This answer helped me solve this problem. My fixed code looks like this:

    func expandCollectionView() {
let screenSize: CGRect = UIScreen.mainScreen().bounds

var frame = self.IBcalendarTableView.frame
frame.origin.y = screenSize.height / 2
frame.size.height = frame.origin.y

UIView.animateWithDuration(0.5) { () -> Void in
self.IBcalendarTableView.frame = frame
self.IBtableViewHeightConstraint.constant = screenSize.height/2
}

}

UITableViewCell expanding animation with self-sizing labels, visual animation problem

You can almost fix your issues with very few changes.

Two important things:

  • For all labels, set both Hugging and Compression Resistance to 1000 (Required)
  • Make sure you have a complete vertical constraint chain.

Couple drawbacks to your method of setting / clearing the text of the label though. The expand animation will work well, but the collapse animation will look as it does in your original gif --- the text disappears instead of being covered. Also, you have extra spacing at the bottom.

To get around that, you can set a constraint from the bottom of the "description" label - as you likely have it now - to the bottom of the content view... this will be the "expanded" constraint, AND add another constraint from the bottom of the "center 0" label to the bottom of the content view... this will be the "collapsed" constraint.

Those constraints will conflict at first... so change the Priority of the one of those constraints (doesn't matter which) to 750 (Default High) and the other constraint to 250 (Default Low).

When your app is running, you would swap the priorities based on whether you want the cell expanded or collapsed.

I put together a sample app using your layout - You can download it here: https://github.com/DonMag/Maverick

I use background colors to make it easy to see the frames. There is a line in cellForRowAt that can be un-commented to clear the colors.

The result using your set / clear text approach:

and using the Constraints method:

How to resize UITableViewCell height by showing and hiding components using auto layout

Do following steps:

  1. When you hide any component give that component frame height as 0 and reload tableview. If you giving any component constant height then take outlet of height constraint and make it zero and specify its constant height again when you unhide.

  2. When you unhide give him specific frame height and reload tableView.

  3. heightForRow must returnUITableViewAutomaticDimension

As you already taken components from the storyboard so compiler understands the height of cell and work accordingly.

If you still facing issue you can ask.



Related Topics



Leave a reply



Submit