13

I'm having a problem in animating the addition or removal of a row in a UITableView which has a different height than other rows.

The following gifs demonstrats the issue with rows of the default height (44pts) and an larger row (100pts) being inserted and removed. The one on the left is a screen recording from the simulator (the new cell ending up covering row five is a different issue) and the one on the right is a mockup of what it should do.

gif of animation issuegif of how it should look

In my case, I have a bunch of rows, each 60pts in height. When a button in the cell is tapped, an "edit" cell will slide out from underneath, pushing lower cells down. This edit cell is 180pts high. When I call insertRowsAtIndexPaths:withRowAnimation: or deleteRowsAtIndexPaths:withRowAnimation:, the animation assumes the wrong height of 60pts, instead of the 180pts it should be
This means that in the case of UITableViewRowAnimationTop the new cell appears at -60pts from the position it will end up at, and slides down to its new position; about a third of the animation it should be doing. Meanwhile, the row below animates smoothly from its starting position to 180pts downward, exactly as it should.

Has anyone worked out an actual solution to this? some way to tell the new row what hight it's supposed to be for the animation?

Below is the code I am using to hide and show the edit row. I'm using a TLSwipeForOptionsCell to trigger the edit, but it's easily replicated using for example tableView:didSelectRowAtIndexPath:

-(void)hideEditFields{
    [self.tableView beginUpdates];

    [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
    editFormVisibleForRow = -1;

    [self.tableView endUpdates];
}

-(void)cellDidSelectMore:(TLSwipeForOptionsCell *)cell{
    NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
    // do nothing if this is the currently selected row
    if(editFormVisibleForRow != indexPath.row){
        if(editFormVisibleForRow >= 0){
            [self hideEditFields];
            // update the index path, as the cell positions (may) have changed
            indexPath = [self.tableView indexPathForCell:cell];
        }

        [self.tableView beginUpdates];

        editFormVisibleForRow = indexPath.row;
        [self.tableView insertRowsAtIndexPaths:@[
                                             [NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]
                                             ] withRowAnimation:UITableViewRowAnimationTop];

        [self.tableView endUpdates];
    }
}

-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return _dataSource.count + (editFormVisibleForRow >= 0 ? 1 : 0);
}

-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    int row = indexPath.row;
    if(editFormVisibleForRow >= 0 && row > editFormVisibleForRow && row <= editFormVisibleForRow + 1){
        return 180.0f;
    }
    else return 60.0;
}

Poking around a bit, it seems like this is a common issue with no clear answer. Most of the similar questions I've found here on SO are unanswered or offer workarounds specific to the asker's situation. (examples: Problem with RowAnimation, Custom UITableViewCell height yields improper animation, UITableView animation glitch when deleting and inserting cells with varying heights).

Also, instead of trying to make one triple-sized edit row, I tried making three smaller rows and animating them, but this was not suitable because they all appeared at once. I also tried animating them one after the other but the easing made it look odd, with an obvious 3-step animation occurring, instead of the whole edit view sliding out of view in one motion.

Edit: I've just noticed that if I call reloadRowsAtIndexPaths:withRowAnimation: UITableViewRowAnimationNone for the row above the one I'm trying to animate, it changes the behaviour of the animation; namely the animation assumes the height is 0pts, as demonstrated in the following animation. It's closer to what I want, but still not right, as the animation speed is wrong and it leaves a gap (in my app this means the background colour pokes through)

gif of issue with previous row reloaded

Community
  • 1
  • 1
death_au
  • 1,282
  • 2
  • 20
  • 41

2 Answers2

0

The solution is pretty straight forward. You need to insert the cell with a height of 0, then change the height to the expected size and then call beginUpdates and endUpdates.

Here is some pseudo code.

var cellHeight: CGFloat = 0

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let dynamicHeightIndex = 2
    if indexPath.row == dynamicHeightIndex {
        return cellHeight
    } else {
        return tableView.rowHeight
    }
}

func insertCell() {
    // First update the data source before inserting the row
    tableView.insertRows(at: [someIndexPath], with: .none)

    cellHeight = 200

    tableView.beginUpdates()
    tableView.endUpdates()
}

To remove the cell, you'll need to wait until the updates animation completes before removing from the table view.

In iOS 11 you have the func performBatchUpdates(_:completion:) which provides a completion block. For previous versions you can try using the CATransaction completion.

cellHeight = 0

CATransaction.begin()
CATransaction.setCompletionBlock({
    self.tableView.deleteRows(at: [someIndexPath], with: .none)
})

tableView.beginUpdates()
tableView.endUpdates()

CATransaction.commit()
cnotethegr8
  • 7,342
  • 8
  • 68
  • 104
-1

This, using didSelectRowAtIndexPath worked for me:

@interface TableController ()
@property (strong,nonatomic) NSArray *theData;
@property (strong,nonatomic) NSIndexPath *pathToEditCell;
@end

@implementation TableController 


- (void)viewDidLoad {
    [super viewDidLoad];
    self.theData = @[@"One",@"Two",@"Three",@"Four",@"Five",@"Six",@"Seven",@"Eight",@"Nine"];
    [self.tableView reloadData];
}


-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return ([indexPath isEqual:self.pathToEditCell])? 100: 44;
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return (self.pathToEditCell == nil)? self.theData.count: self.theData.count + 1;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if ([indexPath isEqual:self.pathToEditCell]) {
        RDEditCell *cell = [tableView dequeueReusableCellWithIdentifier:@"EditCell" forIndexPath:indexPath];
        return cell;
    }else{
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
        cell.textLabel.text = self.theData[indexPath.row];
        return cell;
    }
}


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

    if (self.pathToEditCell == nil) { // first time selecting a row
        self.pathToEditCell = [NSIndexPath indexPathForRow:indexPath.row +1 inSection:indexPath.section];
        [self.tableView insertRowsAtIndexPaths:@[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationAutomatic];
    }else if ([self.pathToEditCell isEqual:indexPath]){ // deletes the edit cell if you click on it
        self.pathToEditCell = nil;
        [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];

    }else{ // close the old edit cell and adds another if you click on another cell while the edit cell is on screen
        [self.tableView beginUpdates];
        [self.tableView deleteRowsAtIndexPaths:@[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationFade];
        self.pathToEditCell = indexPath;
        [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
        [self.tableView endUpdates];
    }
}

For the deletions, I like the looks of the "fade" option for the animation, but "top" also was ok.

rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • 1
    Your code has the same issue. The edit cell only slides out from -44pts, rather than 100pts, which makes the cell look like it's expanding from the center rather than sliding down. Here's a gif to demonstrate the issue: http://i34.photobucket.com/albums/d135/death_au/cell-anim-issue.gif – death_au Nov 15 '13 at 04:54
  • @death_au, ok, I think I misunderstood your problem -- I thought you were only seeing part of the new cell. Personally, I don't see anything wrong with that animation -- it looks ok to me. I didn't even notice what you were talking about, until I slowed it down. Anyway, I don't know a way around this issue. – rdelmar Nov 15 '13 at 05:04