Cancel a UIView animation?
Asked Answered
C

18

258

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.

Consonant answered 16/2, 2009 at 23:19 Comment(8)
Do you mean pausing the animation in the middle and leaving it there? Or going back to where it was at the start?Uraemia
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.Consonant
@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.Consonant
@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.Consonant
- 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.Consonant
- just to clarify - it jumps to the end position unless you also use setAnimationBeginsFromCurrentState in the second animation blockConsonant
A similar question was asked and answered here.Melpomene
Hey I think Tiago Almeida's answer should be the accepted one now.Meryl
A
116

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];
Adiaphorous answered 8/5, 2009 at 21:46 Comment(6)
Thanks Stephen. Actually if you read the comments under my question this is exactly what I did do in the end - but since you've given a good, clear, account of it here I'm marking it as acceptedConsonant
@Vojto How so? The docs say that the use of setAnimationBeginsFromCurrentState: is discouraged from iOS 4 but it's still there and should still function.Adiaphorous
in iOS4+, use the UIViewAnimationOptionBeginFromCurrentState option on animateWithDuration:delay:options:animations:completion:Lanta
Will UIViewAnimationOptionBeginFromCurrentState cancel any animation in progress or just an animation that targets the same property? If it's any animation, how can you make it continue a new animation at the same point WITHOUT stopping the same animation in progress?Mooney
@yourfriendzak, based on a brief test, it looks like it only cancels an animation targeting the same property. I tried changing the alpha of a scrollView whose contentOffset was animated and the contentOffset animation continued.Semiaquatic
Does the cut short animation trigger its completion block?Mooney
R
366

Use:

#import <QuartzCore/QuartzCore.h>

.......

[myView.layer removeAllAnimations];
Rafa answered 19/3, 2010 at 6:53 Comment(5)
I found that I had to add #import <QuartzCore/QuartzCore.h> to remove a compiler warning ;)Micco
So simple, and yet so hard to find. Thank you for this, I've just spent an hour trying to figure this out.Afrit
Possible problem with this solution is that (in iOS 5 at least) there's a delay before it takes effect - it doesn't work soon enough if what you mean is "stop the animation right now, before I go on to the next line of code".Hildy
I found that calling this triggers the completion:^(BOOL finished) block if using + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completionOrpine
@Hildy do u know a solution that stop right away the animation? i am observing a small delay too before the animation really stop.Narrative
A
116

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];
Adiaphorous answered 8/5, 2009 at 21:46 Comment(6)
Thanks Stephen. Actually if you read the comments under my question this is exactly what I did do in the end - but since you've given a good, clear, account of it here I'm marking it as acceptedConsonant
@Vojto How so? The docs say that the use of setAnimationBeginsFromCurrentState: is discouraged from iOS 4 but it's still there and should still function.Adiaphorous
in iOS4+, use the UIViewAnimationOptionBeginFromCurrentState option on animateWithDuration:delay:options:animations:completion:Lanta
Will UIViewAnimationOptionBeginFromCurrentState cancel any animation in progress or just an animation that targets the same property? If it's any animation, how can you make it continue a new animation at the same point WITHOUT stopping the same animation in progress?Mooney
@yourfriendzak, based on a brief test, it looks like it only cancels an animation targeting the same property. I tried changing the alpha of a scrollView whose contentOffset was animated and the contentOffset animation continued.Semiaquatic
Does the cut short animation trigger its completion block?Mooney
H
64

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.

Hildy answered 3/1, 2012 at 23:3 Comment(7)
I've been using [CATransaction flush] after your code which works great, sometimes cause it doesn't write immediately. But however, there's a 0.1 second performance issue, e.g. it lags for that particular time. Any workarounds?Dodecagon
removeAllAnimations will have the side effect of bringing the animation to its final frame, rather than stopping it where it is right now.Barajas
@Barajas That's irrelevant. If your desire is to remove the animations and return the view to its initial position, then say removeAllAnimations and return the view to its initial position.Hildy
Normally, when one wants an animation to stop, they expect it to stop at its current frame -- not first frame nor last frame. Jumping to first or last frame will look like jerky motion.Barajas
I'd detail the answer then to specify what's the default behavior and how it can be changed. Obviously someone doing animation is actively interested in what happens on the screen following his method calls :)Barajas
I have detailed it elsewhere. That is not what was asked so it isn't what I answered here. If you have another answer, definitely give it as an answer!Hildy
This should be the right answer, I think. Thank You.Conjunctive
D
49

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;
                     }
                 }];
}
Dichromate answered 11/8, 2011 at 8:30 Comment(0)
S
42

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.

Spider answered 19/6, 2010 at 17:9 Comment(3)
This is only partially true. It does stop the animation, but it does not stop it from firing delegate methods, most notably, the animationComplete handler, so if you have logic there, you will see problems.Grendel
That's what the 'finished' argument in -animationDidStop:finished:context: is for. If [finished boolValue] == NO, then you know that your animation was somehow cancelled.Annieannihilate
this is a great tip from 7 years ago! note there's a weird behavior: say you are animating alpha back and fore, and you make it "go to 0". to STOP the animation, you can't set alpha to zero; you set it to say 1.Oden
O
38

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!

Obeded answered 9/11, 2017 at 17:57 Comment(2)
This is definitely the most relevant for modern animations.Inaptitude
THIS IS AWESOME! This is perfect for animating UIScrollView.contentOffset, because, by default, it allows user interaction, unlike UIView.animate, which blocks triggering to the panGestureRecognizer in UIScrollView. In fact the fact that UIView.animate blocked the panGestureRecognizer is why I wanted to be able to cancel the animation in the first place; and with this I don't need to! The dampening effect is great too. It solves some really daunting problems implicitly.Mannikin
A
12

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];
}
Algid answered 7/11, 2014 at 17:21 Comment(2)
Also only self.constraintView.layer.removeAllAnimations() works (Swift)Toxicosis
None of the other solutions worked.Thanks, it worked for me.Phobia
A
8

If you just want to pause/stop animation smoothly

self.yourView.layer.speed = 0;

Source: How to pause the animation of a layer tree

Allocution answered 29/6, 2015 at 11:2 Comment(0)
L
5

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.

Loyalist answered 16/8, 2011 at 17:21 Comment(0)
P
5
[UIView setAnimationsEnabled:NO];
// your code here
[UIView setAnimationsEnabled:YES];
Pardo answered 6/2, 2015 at 5:30 Comment(0)
H
2

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()
Homologue answered 26/12, 2017 at 15:9 Comment(0)
O
1

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.

Olympia answered 8/5, 2009 at 17:3 Comment(0)
G
1

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.

Gluttonize answered 5/10, 2010 at 8:52 Comment(2)
NickForge explains this perfectly in the comment below TomTaylor's correct answer above...Oden
Thanks a lot for this note! I indeed have such a situation, where a next animation is fired up when animationDidStop is called. If you simply remove an animation and immediately re-add a new one for the same key I find it will not work. You need to wait, for example till animationDidStop triggers for another animation, or something else. The animation that was just removed will not fire animationDidStop any more.Rugby
R
0
CALayer * pLayer = self.layer.presentationLayer;
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView animateWithDuration:0.001 animations:^{
    self.frame = pLayer.frame;
}];
Rato answered 28/1, 2013 at 14:54 Comment(0)
G
0

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;
Guyguyana answered 3/12, 2015 at 21:43 Comment(0)
M
0

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)
}
Madera answered 7/2, 2019 at 9:3 Comment(0)
R
0

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

    }

}
Regardless answered 20/8, 2019 at 11:52 Comment(0)
V
0

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)
}
Vast answered 1/6, 2022 at 7:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.