120

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?

pixelfreak
  • 17,714
  • 12
  • 90
  • 109

7 Answers7

298

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.

Rudolf Adamkovič
  • 31,030
  • 13
  • 103
  • 118
  • 7
    Didn't work for me. Completion block is called while the animation is still running. – tonymontana Feb 19 '13 at 12:56
  • 2
    @MrVincenzo Did you set the `completionBlock` before `beginUpdates` and `endUpdates` like in the snippet? – Rudolf Adamkovič Feb 19 '13 at 15:11
  • Yes. I am implementing the `NSFetchedResultsControllerDelegate` protocol. `[CATransaction begin]` is called in `controllerWillChangeContent` just before `[tableView beginUpdates]`. Same thing in `controllerDidChangeContent`. `[CATransaction commit]` is called before `[tableView endUpdates]`. – tonymontana Feb 19 '13 at 16:47
  • 2
    `[CATransaction commit]` should be called after not before `[tableView endUpdates]`. – Rudolf Adamkovič Feb 19 '13 at 17:12
  • My mistake. It is called after as you suggested. I already reverted this aforementioned code. I will give it another try and update. Thanks. – tonymontana Feb 19 '13 at 17:25
  • Rudolf Adamkovic, would you mind explain how this works, maybe update in your answer? – jasondinh Mar 25 '13 at 09:56
  • 6
    Completion block is never called if a cell contains a view with animations, ie UIActivityIndicatorView. – markturnip Mar 14 '14 at 06:51
  • Thanks! Works nice with `-reloadData` too so I can get correct content size right after table end reloading and set content align to bottom of screen – aquarium_moose Jun 30 '15 at 11:19
  • 3
    After all this time I never knew this. Pure Gold !! Nothing worse then seeing a nice animation getting killed with the necessary evil of tableView.reloadData(). – DogCoffee Sep 21 '15 at 12:46
  • Works for me only if I set all CATransaction actions after endUpdates – Алексей Абдулин Nov 16 '16 at 12:01
  • should probably set "[myUITableView setUserInteractionEnabled:NO]" before the animation begins and undo this on completion. – Peter Jul 14 '17 at 10:15
34

Swift Version


CATransaction.begin()

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

tableView.beginUpdates()
tableView.endUpdates()

CATransaction.commit()
Michael
  • 9,639
  • 3
  • 64
  • 69
7

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
})
Robert
  • 5,735
  • 3
  • 40
  • 53
5

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.

Community
  • 1
  • 1
Zdenek
  • 3,653
  • 27
  • 34
5

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;
    }
}
1

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

pixelfreak
  • 17,714
  • 12
  • 90
  • 109
  • 1
    `scrollViewDidEndScrollingAnimation` is only called in response to `setContentOffset` and `scrollRectToVisible` – samvermette Jan 08 '12 at 23:12
  • @samvermette Well, in my case, when beginUpdates/endUpdates are called, the UITableView always animate-scroll. But this is specific to my implementation, so I agree it's not the best answer but it works for me. – pixelfreak Jan 11 '12 at 19:29
  • Seems it is possible to enclose updates into an UIView animation block. See my answer bellow: http://stackoverflow.com/a/14305039/634940. – Zdenek Jan 13 '13 at 15:44
0

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];
    }
}
chown
  • 51,908
  • 16
  • 134
  • 170
  • Thanks, chown. Don't think `willDisplayCell` will work. I need it to happen after the animation because I need to make a visual update only after everything settles. – pixelfreak Oct 02 '11 at 01:46
  • np @pixelfreak, if I think of any other way to do this I'll let ya know. – chown Oct 02 '11 at 01:56