119
votes

I am inserting/deleting table cell using insertRowsAtIndexPaths/deleteRowsAtIndexPaths wrapped in beginUpdates/endUpdates. I am also using beginUpdates/endUpdates when adjusting rowHeight. All these operations are animated by default.

How can I detect that animation has ended when using beginUpdates/endUpdates?

7
FYI: This applies to -scrollToRowAtIndexPath:atScrollPosition:animated: as well.Ben Lachman

7 Answers

296
votes

What about this?

[CATransaction begin];

[CATransaction setCompletionBlock:^{
    // animation has finished
}];

[tableView beginUpdates];
// do some work
[tableView endUpdates];

[CATransaction commit];

This works because the tableView animations use CALayer animations internally. That is, they add the animations to any open CATransaction. If no open CATransaction exists (the normal case), then one is implicitly began, which is ended at the end of the current runloop. But if you begin one yourself, like is done here, then it will use that one.

34
votes

Swift Version


CATransaction.begin()

CATransaction.setCompletionBlock({
    do.something()
})

tableView.beginUpdates()
tableView.endUpdates()

CATransaction.commit()
7
votes

If you're targeting iOS 11 and above, you should use UITableView.performBatchUpdates(_:completion:) instead:

tableView.performBatchUpdates({
    // delete some cells
    // insert some cells
}, completion: { finished in
    // animation complete
})
5
votes

A possible solution could be to inherit from the UITableView on which you call endUpdates and overwrite its setContentSizeMethod, since UITableView adjusts its content size to match the added or removed rows. This approach should also work for reloadData.

To ensure that a notification is sent only after endUpdates is called, one could also overwrite endUpdates and set a flag there.

// somewhere in header
@private BOOL endUpdatesWasCalled_;

-------------------

// in implementation file

- (void)endUpdates {
    [super endUpdates];
    endUpdatesWasCalled_ = YES;
}

- (void)setContentSize:(CGSize)contentSize {
    [super setContentSize:contentSize];

    if (endUpdatesWasCalled_) {
        [self notifyEndUpdatesFinished];
        endUpdatesWasCalled_ = NO;
    }
}
5
votes

You can enclose your operation(s) in UIView animation block like so:

- (void)tableView:(UITableView *)tableView performOperation:(void(^)())operation completion:(void(^)(BOOL finished))completion
{
    [UIView animateWithDuration:0.0 animations:^{

        [tableView beginUpdates];
        if (operation)
            operation();
        [tableView endUpdates];

    } completion:^(BOOL finished) {

        if (completion)
            completion(finished);
    }];
}

Credits to https://stackoverflow.com/a/12905114/634940.

1
votes

Haven't found a good solution yet (short of subclassing UITableView). I've decided to use performSelector:withObject:afterDelay: for now. Not ideal, but gets the job done.

UPDATE: It looks like I can use scrollViewDidEndScrollingAnimation: for this purpose (this is specific to my implementation, see comment).

0
votes

You can use tableView:willDisplayCell:forRowAtIndexPath: like:

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"tableView willDisplay Cell");
    cell.backgroundColor = [UIColor colorWithWhite:((indexPath.row % 2) ? 0.25 : 0) alpha:0.70];
}

But this will also get called when a cell that is already in the table moves from off the screen to on the screen so it may not be exactly what you are looking for. I just looked through all the UITableView and UIScrollView delegate methods and there doesnt appear to be anything to handle just after a cell is inserted animation.


Why not just call the method you want to be called when the animation ends after the endUpdates?

- (void)setDownloadedImage:(NSMutableDictionary *)d {
    NSIndexPath *indexPath = (NSIndexPath *)[d objectForKey:@"IndexPath"];
    [indexPathDelayed addObject:indexPath];
    if (!([table isDragging] || [table isDecelerating])) {
        [table beginUpdates];
        [table insertRowsAtIndexPaths:indexPathDelayed withRowAnimation:UITableViewRowAnimationFade];
        [table endUpdates];
        // --> Call Method Here <--
        loadingView.hidden = YES;
        [indexPathDelayed removeAllObjects];
    }
}