104

Is there a way to either specify the duration for UITableView row animations, or to get a callback when the animation completes?

What I would like to do is flash the scroll indicators after the animation completes. Doing the flash before then doesn't do anything. So far the workaround I have is to delay half a second (that seems to be the default animation duration), i.e.:

[self.tableView insertRowsAtIndexPaths:newRows
                      withRowAnimation:UITableViewRowAnimationFade];
[self.tableView performSelector:@selector(flashScrollIndicators)
                     withObject:nil
                     afterDelay:0.5];
TheNeil
  • 3,321
  • 2
  • 27
  • 52
Daniel Dickison
  • 21,832
  • 13
  • 69
  • 89
  • I haven't tried myself, but maybe this could do it, with some index path handling: `- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath` – Kalle Oct 07 '12 at 07:13

11 Answers11

212

Just came across this. Here's how to do it:

Objective-C

[CATransaction begin];
[tableView beginUpdates];
[CATransaction setCompletionBlock: ^{
    // Code to be executed upon completion
}];
[tableView insertRowsAtIndexPaths: indexPaths
                 withRowAnimation: UITableViewRowAnimationAutomatic];
[tableView endUpdates];
[CATransaction commit];

Swift

CATransaction.begin()
tableView.beginUpdates()
CATransaction.setCompletionBlock {
    // Code to be executed upon completion
}
tableView.insertRowsAtIndexPaths(indexArray, withRowAnimation: .Top)
tableView.endUpdates()
CATransaction.commit()
edzio27
  • 4,126
  • 7
  • 33
  • 48
karwag
  • 2,641
  • 1
  • 15
  • 12
  • On further experimentation, the behavior of this mechanism in this context seems erratic at best. Note also that the documentation says that it applies only to animations added after the call. – JLundell Nov 08 '12 at 00:52
  • Yes, you're right about that. Setting the completion block before the call to insertRowsAtIndexPaths is how it should be done (also added a CATransaction begin/commit to make sure). This is exactly how I've implemented it in my apps and it works flawlessly. Nice if you want to chain an animation off the insert/delete animation. – karwag Nov 19 '12 at 03:23
  • FWIW, this does not work for me. The callback gets hit long, long after the actual animation is complete. (iOS 6) – jimt Dec 22 '12 at 09:23
  • 2
    Again, works flawlessly here. iOS6 and all. This is a proper SDK-supported mechanism for overriding properties in default animations. Perhaps you have additional, longer-running animations inside your CATransaction? They get nested, you know. – karwag Dec 22 '12 at 17:31
  • 5
    `setAnimationDuration` doesn't seem to affect the insert/delete duration. iOS 6 – Tom Redman Feb 06 '13 at 15:18
  • 5
    Works fine for me too in iOS 5.1.1, 6.1, 7.0; But, if you need to get a new tableView.contentSize after animation (as it was in my case), you must use [self performSelectorOnMainThread:withObject:waitUntilDone:]; in setCompletionBlock in order to call your delegate in next runloop. if you call your delegate directly, without performSelectorOnMainThread, you get old value for tableView.contentSize. – slamor Sep 18 '13 at 16:56
  • 1
    This seems to work without [tableView beginUpdates] and [tableView endUpdate] method call as well . – Prajeet Shrestha Jun 06 '16 at 07:20
  • Be careful to not set the completion block after performing your animations. If you set the completion block after, it will be executed immediately. – hetelek Feb 10 '17 at 01:10
  • Not having used it myself yet, but in iOS 11 a new function `performBatchUpdates` should be preferred over `beginUpdates`: https://developer.apple.com/documentation/uikit/uitableview/2887515-performbatchupdates – Jonny Feb 16 '18 at 02:22
  • beginUpdates doesn't need to be after begin – malhal Jun 09 '20 at 17:58
40

Expanding on karwag's fine answer, note that on iOS 7, surrounding the CATransaction with a UIView Animation offers control of the table animation duration.

[UIView beginAnimations:@"myAnimationId" context:nil];

[UIView setAnimationDuration:10.0]; // Set duration here

[CATransaction begin];
[CATransaction setCompletionBlock:^{
    NSLog(@"Complete!");
}];

[myTable beginUpdates];
// my table changes
[myTable endUpdates];

[CATransaction commit];
[UIView commitAnimations];

The UIView animation's duration has no effect on iOS 6. Perhaps iOS 7 table animations are implemented differently, at the UIView level.

Community
  • 1
  • 1
Brent
  • 1,149
  • 8
  • 11
29

That's one hell of a useful trick! I wrote a UITableView extension to avoid writing CATransaction stuff all the time.

import UIKit

extension UITableView {

    /// Perform a series of method calls that insert, delete, or select rows and sections of the table view.
    /// This is equivalent to a beginUpdates() / endUpdates() sequence, 
    /// with a completion closure when the animation is finished.
    /// Parameter update: the update operation to perform on the tableView.
    /// Parameter completion: the completion closure to be executed when the animation is completed.
   
    func performUpdate(_ update: ()->Void, completion: (()->Void)?) {
    
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)

        // Table View update on row / section
        beginUpdates()
        update()
        endUpdates()
    
        CATransaction.commit()
    }

}

This is used like so:

// Insert in the tableView the section we just added in sections
self.tableView.performUpdate({
    self.tableView.insertSections([newSectionIndex], with: UITableViewRowAnimation.top)

}, completion: {
    // Scroll to next section
    let nextSectionIndexPath = IndexPath(row: 0, section: newSectionIndex)
    self.tableView.scrollToRow(at: nextSectionIndexPath, at: .top, animated: true)
})
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
26

Shortening Brent's fine answer, for at least iOS 7 you can wrap this all tersely in a [UIView animateWithDuration:delay:options:animations:completion:] call:

[UIView animateWithDuration:10 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
  [self.tableView beginUpdates];
  [self.tableView endUpdates];
} completion:^(BOOL finished) {
  // completion code
}];

though, I can't seem to override the default animation curve from anything other than EaseInOut.

Community
  • 1
  • 1
visnup
  • 499
  • 4
  • 5
  • 3
    When doing a row insert this way, or @Brent's way, although the duration is respected, the UITableViewRowAnimation does not seem to be respected and always appears to animate top down, even when I specify, for example UITableViewRowAnimationLeft. Testing on iOS 8.4 - anybody have a solution? – Danny Jul 22 '15 at 04:02
23

Here's a Swift version of karwag's answer

CATransaction.begin()
tableView.beginUpdates()
CATransaction.setCompletionBlock { () -> Void in
    // your code here
}
tableView.insertRowsAtIndexPaths(indexArray, withRowAnimation: .Top)
tableView.endUpdates()
CATransaction.commit()
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
primulaveris
  • 966
  • 14
  • 20
7

For me I needed this for a collectionView. I've made a simple extension to solve this:

extension UICollectionView {

    func reloadSections(sections: NSIndexSet, completion: () -> Void){
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)

        self.reloadSections(sections)

        CATransaction.commit()
    }

}
Antoine
  • 23,526
  • 11
  • 88
  • 94
3

Nowadays if you want to do this there is new function starting from iOS 11:

- (void)performBatchUpdates:(void (^)(void))updates 
                 completion:(void (^)(BOOL finished))completion;

In updates closures you place the same code as in beginUpdates()/endUpdates section. And the completion is executed after all animations.

Daniel Dickison
  • 21,832
  • 13
  • 69
  • 89
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143
1

As tableView's performBatch method is available starting from iOS 11 only, you can use following extension:

extension UITableView {
func performUpdates(_ updates: @escaping () -> Void, completion: @escaping (Bool) -> Void) {
        if #available(iOS 11.0, *) {
            self.performBatchUpdates({
                updates()
            }, completion: completion)
        } else {
            CATransaction.begin()
            beginUpdates()
            CATransaction.setCompletionBlock {
                completion(true)
            }
            updates()
            endUpdates()
            CATransaction.commit()
        }
    }
}
Stanislau Baranouski
  • 1,425
  • 1
  • 18
  • 22
0

Antoine's answer is pretty good – but is for UICollectionView. Here it is for UITableView:

extension UITableView {
    func reloadSections(_ sections: IndexSet, with rowAnimation: RowAnimation, completion: (() -> Void)?) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        
        self.reloadSections(sections, with: rowAnimation)
        
        CATransaction.commit()
    }
}

Called like so:

tableView.reloadSections(IndexSet(0), with: .none, completion: {
    // Do the end of animation thing        
})
adamjansch
  • 1,170
  • 11
  • 22
0

If someone is facing the problem when tableView is ignoring animation parameters from UIView.animate and using "from up to down" default animation for reloading rows, I've found a strange solution:

You need to:

  1. Silence tableView animation
  2. Use transitionAnimation instead

Example:

let indicesToUpdate = [IndexPath(row: 1, section: 0)]
UIView.transition(with: self.tableView,
                      duration: 0.5,
                      options: [.transitionCrossDissolve,
                                .allowUserInteraction,
                                .beginFromCurrentState],
                      animations: {
                        UIView.performWithoutAnimation {
                            self.tableView.reloadRows(at: indicesToUpdate,
                                                      with: .none)
                        }
                      })

PS: UIView.transition(..) also has optional completion :)

-9

You could try to wrap the insertRowsAtIndexPath in a

- (void)beginUpdates
- (void)endUpdates

transaction, then do the flash afterwards.

Jordan
  • 21,746
  • 10
  • 51
  • 63