249
votes

Is it possible to cancel a UIView animation while it is in progress? Or would I have to drop to the CA level?

i.e. I've done something like this (maybe setting an end animation action too):

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:duration];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
// other animation properties

// set view properties

[UIView commitAnimations];

But before the animation completes and I get the animation ended event, I want to cancel it (cut it short). Is this possible? Googling around finds a few people asking the same question with no answers - and one or two people speculating that it can't be done.

17
Do you mean pausing the animation in the middle and leaving it there? Or going back to where it was at the start?Chris Lundie
Either leaving it exactly where it is, mid-animation (in practice I'll be starting another animation straight after anyway), or jump straight to the end point. I'd imagine the first is more natural, but either works for me in this case.philsquared
@Chris Hanson: I appreciate your edits but wonder what your rationale is for removing the iPhone tag. It may be implied by cocoa-touch, but I for one have a tag filter on iPhone and would miss this in that case.philsquared
@Boot To The Head (again): your question, and my own response to it, prompted me to test in isolation a bit more and I discovered if you start a new animation before the first one has finished it does jump to the end point before starting the new one.philsquared
- this meets my immediate needs (and suggests I have some other bug in my real code), but I'm going to leave this question open as I know others have been asking for the same.philsquared

17 Answers

361
votes

Use:

#import <QuartzCore/QuartzCore.h>

.......

[myView.layer removeAllAnimations];
115
votes

The way I do it is to create a new animation to your end point. Set a very short duration and make sure you use the +setAnimationBeginsFromCurrentState: method to start from the current state. When you set it to YES, the current animation is cut short. Looks something like this:

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.1];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
// other animation properties

// set view properties

[UIView commitAnimations];
62
votes

Simplest way to stop all animations on a particular view, immediately, is this:

Link the project to QuartzCore.framework. At the start of your code:

#import <QuartzCore/QuartzCore.h>

Now, when you want to stop all animations on a view dead in their tracks, say this:

[CATransaction begin];
[theView.layer removeAllAnimations];
[CATransaction commit];

The middle line would work all by itself, but there's a delay until the runloop finishes (the "redraw moment"). To prevent that delay, wrap the command in an explicit transaction block as shown. This works provided no other changes have been performed on this layer in the current runloop.

48
votes

On iOS 4 and greater, use the UIViewAnimationOptionBeginFromCurrentState option on the second animation to cut the first animation short.

As an example, assume you have a view with an activity indicator. You wish to fade in the activity indicator while some potentially time consuming activity begins, and fade it out when the activity is finished. In the code below, the view with the activity indicator on it is called activityView.

- (void)showActivityIndicator {
    activityView.alpha = 0.0;
    activityView.hidden = NO;
    [UIView animateWithDuration:0.5
                 animations:^(void) {
                     activityView.alpha = 1.0;
                 }];

- (void)hideActivityIndicator {
    [UIView animateWithDuration:0.5
                 delay:0 options:UIViewAnimationOptionBeginFromCurrentState
                 animations:^(void) {
                     activityView.alpha = 0.0;
                 }
                 completion:^(BOOL completed) {
                     if (completed) {
                         activityView.hidden = YES;
                     }
                 }];
}
42
votes

To cancel an animation you simply need to set the property that is currently being animated, outside of the UIView animation. That will stop the animation wherever it is, and the UIView will jump to the setting you just defined.

30
votes

Sorry to resurrect this answer, but in iOS 10 things kinda changed, and now cancelling is possible and you can even cancel gracefully!

After iOS 10 you can cancel animations with UIViewPropertyAnimator!

UIViewPropertyAnimator(duration: 2, dampingRatio: 0.4, animations: {
    view.backgroundColor = .blue
})
animator.stopAnimation(true)

If you pass true it cancels the animation and it stops right where you cancelled it. The completion method will not be called. However, if you pass false you are responsible for finishing the animation:

animator.finishAnimation(.start)

You can finish your animation and stay in the current state (.current) or go to the initial state (.start) or to the end state (.end)

By the way, you can even pause and restart later...

animator.pauseAnimation()
animator.startAnimation()

Note: If you don't want an abrupt cancelling you can reverse your animation or even change your animation after you pause it!

12
votes

If you are animating a constraint by changing the constant instead of a view property none of the other methods work on iOS 8.

Example animation:

self.constraint.constant = 0;
[self.view updateConstraintsIfNeeded];
[self.view layoutIfNeeded];
[UIView animateWithDuration:1.0f
                      delay:0.0f
                    options:UIViewAnimationOptionCurveLinear
                 animations:^{
                     self.constraint.constant = 1.0f;
                     [self.view layoutIfNeeded];
                 } completion:^(BOOL finished) {

                 }];

Solution:

You need to remove the animations from the layers of any views being affected by the constraint change and their sublayers.

[self.constraintView.layer removeAllAnimations];
for (CALayer *l in self.constraintView.layer.sublayers)
{
    [l removeAllAnimations];
}
8
votes

If you just want to pause/stop animation smoothly

self.yourView.layer.speed = 0;

Source: How to pause the animation of a layer tree

5
votes

None of the above solved it for me, but this helped: The UIView animation sets the property immediately, then animates it. It stops the animation when the presentation layer matches the model (the set property).

I solved my issue, which was "I want to animate from where you look like you appear" ('you' meaning the view). If you want THAT, then:

  1. Add QuartzCore.
  2. CALayer * pLayer = theView.layer.presentationLayer;

set the position to the presentation layer

I use a few options including UIViewAnimationOptionOverrideInheritedDuration

But because Apple's documentation is vague, I don't know if it really overrides the other animations when used, or just resets timers.

[UIView animateWithDuration:blah... 
                    options: UIViewAnimationOptionBeginFromCurrentState ... 
                    animations: ^ {
                                   theView.center = CGPointMake( pLayer.position.x + YOUR_ANIMATION_OFFSET, pLayer.position.y + ANOTHER_ANIMATION_OFFSET);
                   //this only works for translating, but you get the idea if you wanna flip and scale it. 
                   } completion: ^(BOOL complete) {}];

And that should be a decent solution for now.

5
votes
[UIView setAnimationsEnabled:NO];
// your code here
[UIView setAnimationsEnabled:YES];
2
votes

Swift version of Stephen Darlington's solution

UIView.beginAnimations(nil, context: nil)
UIView.setAnimationBeginsFromCurrentState(true)
UIView.setAnimationDuration(0.1)
// other animation properties

// set view properties
UIView.commitAnimations()
1
votes

I have the same problem; the APIs don't have anything to cancel some specific animation. The

+ (void)setAnimationsEnabled:(BOOL)enabled

disables ALL animations, and thus does not work for me. There's two solutions:

1) make your animated object a subview. Then, when you want to cancel the animations for that view, remove the view or hide it. Very simple, but you need to recreate the subview without animations if you need to keep it in view.

2) repeat the anim only one, and make a delegate selector to restart the anim if needed, like this:

-(void) startAnimation {
NSLog(@"startAnim alpha:%f", self.alpha);
[self setAlpha:1.0];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:YES];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(pulseAnimationDidStop:finished:context:)];
[self setAlpha:0.1];
[UIView commitAnimations];
}

- (void)pulseAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
if(hasFocus) {
    [self startAnimation];
} else {
    self.alpha = 1.0;
}
}

-(void) setHasFocus:(BOOL)_hasFocus {
hasFocus = _hasFocus;
if(hasFocus) {
    [self startAnimation];
}
}

Problem with 2) is that there's always delay stopping the anim as it finishes the current animation cycle.

Hope this helps.

1
votes

Even if you cancel the animation in the ways above animation didStopSelector still runs. So if you have logic states in your application driven by animations you will have problems. For this reason with the ways described above I use the context variable of UIView animations. If you pass the current state of your program by the context param to the animation, when the animation stops your didStopSelector function may decide if it should do something or just return based on the current state and the state value passed as context.

0
votes
CALayer * pLayer = self.layer.presentationLayer;
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView animateWithDuration:0.001 animations:^{
    self.frame = pLayer.frame;
}];
0
votes

To pause an animation without reverting state to original or final:

CFTimeInterval paused_time = [myView.layer convertTime:CACurrentMediaTime() fromLayer:nil];
myView.layer.speed = 0.0;
myView.layer.timeOffset = paused_time;
0
votes

When I work with UIStackView animation, besides removeAllAnimations() I need to set some values to initial one because removeAllAnimations() can set them to unpredictable state. I have stackView with view1 and view2 inside, and one view should be visible and one hidden:

public func configureStackView(hideView1: Bool, hideView2: Bool) {
    let oldHideView1 = view1.isHidden
    let oldHideView2 = view2.isHidden
    view1.layer.removeAllAnimations()
    view2.layer.removeAllAnimations()
    view.layer.removeAllAnimations()
    stackView.layer.removeAllAnimations()
    // after stopping animation the values are unpredictable, so set values to old
    view1.isHidden = oldHideView1 //    <- Solution is here
    view2.isHidden = oldHideView2 //    <- Solution is here

    UIView.animate(withDuration: 0.3,
                   delay: 0.0,
                   usingSpringWithDamping: 0.9,
                   initialSpringVelocity: 1,
                   options: [],
                   animations: {
                    view1.isHidden = hideView1
                    view2.isHidden = hideView2
                    stackView.layoutIfNeeded()
    },
                   completion: nil)
}
0
votes

None of the answered solutions worked for me. I solved my issues this way (I do not know if it is a correct way?), because I had problems when calling this too-fast (when previous animation was not yet finished). I pass my wanted animation with customAnim block.

extension UIView
{

    func niceCustomTranstion(
        duration: CGFloat = 0.3,
        options: UIView.AnimationOptions = .transitionCrossDissolve,
        customAnim: @escaping () -> Void
        )
    {
        UIView.transition(
            with: self,
            duration: TimeInterval(duration),
            options: options,
            animations: {
                customAnim()
        },
            completion: { (finished) in
                if !finished
                {
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    self.layer.removeAllAnimations()
                    customAnim()
                }
        })

    }

}