Swift: How to Animate the Rowheight of a Uitableview

Swift: How to animate the rowHeight of a UITableView?

Don't change the height that way. Instead, when you know you want to change the height of a cell, call (in whatever function):

self.tableView.beginUpdates()
self.tableView.endUpdates()

These calls notify the tableView to check for height changes. Then implement the delegate override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat, and provide the proper height for each cell. The change in height will be animated automatically. You can return UITableViewAutomaticDimension for items you don't have an explicit height for.

I would not suggest doing such actions from within cellForRowAtIndexPath, however, but in one that responds to a tap didSelectRowAtIndexPath, for example. In one of my classes, I do:

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath == self.selectedIndexPath {
self.selectedIndexPath = nil
}else{
self.selectedIndexPath = indexPath
}
}

internal var selectedIndexPath: NSIndexPath? {
didSet{
//(own internal logic removed)

//these magical lines tell the tableview something's up, and it checks cell heights and animates changes
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath == self.selectedIndexPath {
let size = //your custom size
return size
}else{
return UITableViewAutomaticDimension
}
}

how to add animation when table view row height is changing?

You should do like this way, I have made it for simple tableview. Please do appropriate changes.

Declare selectedIndexs like this,

var selectedIndexs: [IndexPath: Bool] = [:]

Implement this method,

func cellIsSelected(indexPath: IndexPath) -> Bool {
if let number = selectedIndexs[indexPath] {
return number
} else {
return false
}
}

Now on didSelectRowAt indexPath

extension ViewController: UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)

let isSelected = !self.cellIsSelected(indexPath: indexPath)
selectedIndexs[indexPath] = isSelected

tableView.beginUpdates()
tableView.endUpdates()
}
}

Implement heightForRowAt indexPath method like this,

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if self.cellIsSelected(indexPath: indexPath) {
return 100
} else {
return 44
}
}

I have animated TableViewCell's height when user tap on any cell in didSelectRowAt indexPath method, you can do it in button tap event also. Make necessary changes and its done.

Let me know in case of any queries.

Swift animate UITableViewCell expanding height constraint

Try without an animation block:

containerHeightConstraint.constant = 410
tableView.beginUpdates()
tableView.endUpdates()

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

UITableViewController: Scrolling to bottom with dynamic row height starts animation at wrong position

Wow, that was a fun challenge. Thanks for posting the test project.

So it seems that after adding the new row there's something off with where the table view thinks it's scrolled to. Seems to me to be a bug in UIKit. So to work around that, I added some code to 'reset' the table view before applying the animation.

Here's what I ended up with:

@IBAction func addMore(sender:UIBarButtonItem) {
let message = [
"title": "haiooo",
"message": "silver"]
messages.append(message)

tableView.reloadData()

// To get the animation working as expected, we need to 'reset' the table
// view's current offset. Otherwise it gets confused when it starts the animation.
let oldLastCellIndexPath = NSIndexPath(forRow: messages.count-2, inSection: 0)
self.tableView.scrollToRowAtIndexPath(oldLastCellIndexPath, atScrollPosition: .Bottom, animated: false)

// Animate on the next pass through the runloop.
dispatch_async(dispatch_get_main_queue(), {
self.scrollToBottom(true)
})
}

I couldn't get it to work with insertRowsAtIndexPaths(_:withRowAnimation:), but reloadData() worked fine. Then you need the same delay again before animating to the new last row.

UITableView with accordion-like animation of cells with dynamic height

I finally solved it building upon beginUpdates/endUpdates. My further research into the problem and solution follows.

The problem

As described in the original question, the expansion phase works correctly. The reason this works is:

  1. When setting the UITextView's isHidden property to false, the containing stack view resizes and gives the cell a new intrinsic content size
  2. Using beginUpdates/endUpdates will animate the change in row heights without reloading the cell. The starting state is that the UITextView's content is visible but currently clipped by the current cell height, since the cell hasn't resized yet.
  3. When endUpdates is called, the cell will automatically expand since the intrinsic size has changed and tableView.rowHeight = .automaticDimension. The animation will gradually reveal the new content as desired.

However, the collapsing phase doesn't yield the same animation effect in reverse. The reason it doesn't is:

  1. When setting the UITextView's isHidden property to true, the containing stack view hides the view and resizes the cell a new intrinsic content size.
  2. When endUpdates is called, the animation will start by removing the UITextView immediately. Thinking about it, this is expected since it's the inverse of what happened during expansion. However, it is also breaking the desired animation effect by leaving the visible elements "hanging" in the middle instead of gradually concealing the UITextView when the row shrinks.

A solution

To get the desired concealing effect the UITextView should stay visible during the whole animation while the cell shrinks. This consists of several steps:

Step 1: Override heightForRow

override func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
// If a cell is collapsing, force it to its original height stored in collapsingRow?
// instead of using the intrinsic size and .automaticDimension
if indexPath == collapsingRow?.indexPath {
return collapsingRow!.height
} else {
return UITableView.automaticDimension
}
}

The UITextView will now stay visible while the cell shrinks, but due to auto layout the UITextView is clipped immediately since it is anchored to the cell height.

Step 2: Create a height constraint on UIStackView and have it take priority while the cell is shrinking

2.1: added a height constraint on UIStackView in Interface builder

2.2: added heightConstraint as a strong (not weak, this is important) outlet in MyTableViewCell

2.3 in awakeFromNib in MyTableViewCell, set heightConstraint.isActive = false to keep the default behavior

2.4 in interface builder: make sure the priority of UIStackView's bottom constraint is set lower than the priority of the height constraint. I.e. 999 for the bottom constraint and 1000 for the height constraint. Failing to do this results in conflicting constraints during the collapsing phase.

Step 3: when collapsing, activate the heightConstraint and set it to UIStackView's current intrinsic size. This keep the contents of UITextView visible while cell height decreases, but also clips the contents as desired, resulting in the "conceal" effect.

if let expandedRow = expandedRow,
let prevCell = tableView.cellForRow(at: expandedRow.indexPath) as? MyTableViewCell {
prevCell.heightConstraint.constant = prevCell.stackView.frame.height
prevCell.heightConstraint.isActive = true

collapsingRow = expandedRow
}

Step 4: reset state when the animation is complete by using CATransaction.setCompletionBlock

The steps combined

class MyTableViewController: UITableViewController {
var expandedRow: (indexPath: IndexPath, height: CGFloat)? = nil
var collapsingRow: (indexPath: IndexPath, height: CGFloat)? = nil

override func viewDidLoad() {
super.viewDidLoad()
self.tableView.backgroundColor = .darkGray
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = 200
}

override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return posts.count
}

override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: "Cell", for: indexPath) as? MyTableViewCell
else { fatalError() }

let post = posts[indexPath.row]
let isExpanded = expandedRow?.indexPath == indexPath
cell.configure(expanded: isExpanded, post: post)

return cell
}

override func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath == collapsingRow?.indexPath {
return collapsingRow!.height
} else {
return UITableView.automaticDimension
}
}

override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let tappedCell = tableView.cellForRow(at: indexPath) as? MyTableViewCell
else { return }

CATransaction.begin()
tableView.beginUpdates()

if let expandedRow = expandedRow,
let prevCell = tableView.cellForRow(at: expandedRow.indexPath)
as? MyTableViewCell {
prevCell.heightConstraint.constant = prevCell.stackView.frame.height
prevCell.heightConstraint.isActive = true

CATransaction.setCompletionBlock {
if let cell = tableView.cellForRow(at: expandedRow.indexPath)
as? MyTableViewCell {
cell.configureExpansion(false)
cell.heightConstraint.isActive = false
}
self.collapsingRow = nil
}

collapsingRow = expandedRow
}

if expandedRow?.indexPath == indexPath {
collapsingRow = expandedRow
expandedRow = nil
} else {
tappedCell.configureExpansion(true)
expandedRow = (indexPath: indexPath, height: tappedCell.frame.height)
}

tableView.endUpdates()
CATransaction.commit()
}
}

Sample Image



Related Topics



Leave a reply



Submit