9
votes

I'm running this animation on a user interaction, and sometimes the animation may be run again before the ongoing animation has finished. What I would like it to do is to cancel the previous animation and continue with the new one.

[UIView animateWithDuration:duration
                      delay:0
                    options:UIViewAnimationOptionBeginFromCurrentState
                 animations:^{
                     self.bounds = bounds;
                 }
                 completion:^(BOOL finished) {
                     if (finished) {
                         // Do some cleanup after animating.
                     }
                 }];

Visually, this seems to work, but in my completion block I am told that it finished in both cases, which causes the cleanup code to run prematurely. So the first animation's completion block runs immediately after the second one starts, with finished = YES. I expected it to have a finished value of NO and the second one (once it completes) to have YES.

Is there a way to know if the animation completed or if it was cancelled by another?

Sidenote: I tried doing the same animation with CABasicAnimation and then I get finished = NO the first time and YES the second time, so the behavior I'm getting seems to be specific to animateWithDuration.

Here's a GIF showing the above code in action with a duration of 10 and the completion block updating the label. As you can see, finished is YES every time the animation is restarted with the animateWithDuration call:

2

2 Answers

13
votes

So I investigated this further and I think this is an iOS 8 specific thing, due to how animations are now additive by default, which means that the animations will continue and add up to each other rather than being cancelled.

In my searching I found a WWDC session called "Building Interruptible and Responsive Interactions" which talks about this. In the session they claim that the animations will all finish at the same time but this is not the case in my testing (see the GIF in the question). The workaround suggested in the session is to keep a counter going for how many times the animation started vs. completed and perform the post-animation actions when the counter reaches zero. This is a bit hacky, but works. Here's the question code adjusted for this:

// Add this property to your class.
self.runningAnimations++;
[UIView animateWithDuration:duration
                      delay:0
                    options:UIViewAnimationOptionBeginFromCurrentState
                 animations:^{
                     self.bounds = bounds;
                 }
                 completion:^(BOOL finished) {
                     if (--self.runningAnimations == 0) {
                         // Do some cleanup after animating.
                     }
                 }];

Obviously you'll need a separate counter for every type of animation so it can get a bit messy in the end, but I don't think there's a better way unless you start using CAAnimation directly.

0
votes

What method have you tried to cancel the animation?

theView_ = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[theView_ setBackgroundColor:[UIColor magentaColor]];
[self.view addSubview:theView_];

[UIView animateWithDuration:10
                 animations:^{
                     theView_.frame = CGRectMake(200, 200, 50, 50);
                 } completion:^(BOOL finished) {
                     NSLog(@"done : %@", @(finished));
                     if(finished) {

                     }
                     else {

                     }
                 }];

[self performSelector:@selector(cancelAnimation) withObject:nil afterDelay:2];

The cancelAnimation method is as below:

- (void)cancelAnimation {
    NSLog(@"cancel");
    [theView_.layer removeAllAnimations];

}

The finished will return false when the animation cancelled.

Log messages are as expected:

2015-02-23 14:13:58.625 TestPrj[47338:6070317] cancel
2015-02-23 14:13:58.626 TestPrj[47338:6070317] done : 0