Cancel a UIView animation?

Viewed 161638

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.

18 Answers

Use:

#import <QuartzCore/QuartzCore.h>

.......

[myView.layer removeAllAnimations];

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];

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!

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.

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

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()
                }
        })

    }

}

If you are looking for flexible animation that can start and stop as per your need, use UIViewPropertyAnimator.

sample code

let sampleView = UIView()

lazy var animator: UIViewPropertyAnimator = {
    let animator = UIViewPropertyAnimator(duration: 1.2, curve: .easeOut)
    animator.isInterruptible = true
    return animator
}()

// add and your animatable property
animator.addAnimations {
   // what you need to animate add here
   sampleView.alpha = 0
}

// start animation
animator.startAnimation(afterDelay: 0.5)

If you need to stop in the middle of the animation. it will stop the animation at its current state.

// stop animation
if animator.isRunning {
    animator.stopAnimation(true)
}
Related