Ecuador
Ecuador

Reputation: 1167

Second animateWithDuration call disables animation

I have what I thought was a very simple case of animation. There is a view that sits at 0 alpha unless it becomes 1 until a future animation with one type of event, or becomes 1 for a few seconds with another type of event. My problem is that when those two events are called consecutively, so, in effect the following code is run:

    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
    self->bkgView.alpha = 1.0;
} completion:^(BOOL finished){}];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
    self->bkgView.alpha = 1.0;
} completion:^(BOOL finished){
    NSLog(@"completion finished: %d", finished);
    if (finished)
        [UIView animateWithDuration:0.5 delay:3 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
            self->bkgView.alpha = 0;
        } completion:^(BOOL finished){}];
}];

I would think that the second call just cancels the first call and takes over from wherever that one was, so I get the flashing to alpha 1 for a few seconds. What actually happens is no animation at all, the completion block is called immediately with finished as true - so no indication it was cancelled. I thought I'd ask instead of banging my head on this seemingly trivial case, what am I doing wrong?

Edit: I meant it when I said that the code is trivial, but here is the entire viewcontroller you can put on a new single view project if you want to try it:

#import "ViewController.h"

@interface ViewController () {
    UIView *bkgView;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    CGFloat x = 20;;
    for (NSString *str in @[@"show", @"flash", @"both"]) {
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        [button addTarget:self action:NSSelectorFromString(str) forControlEvents:UIControlEventTouchUpInside];
        [button setTitle:str forState:UIControlStateNormal];
        button.backgroundColor = [UIColor grayColor];
        button.frame = CGRectMake(x, 100, 80, 40);
        [self.view addSubview:button];
        x += 100;
    }
    bkgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 100)];
    bkgView.backgroundColor = [UIColor blackColor];
    bkgView.alpha = 0;
    [self.view addSubview:bkgView];
}

-(void)both {
    [self show];
    [self flash];
}

-(void)show {
    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
        self->bkgView.alpha = 1.0;
    } completion:^(BOOL finished){}];
}

-(void)flash {
    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
        self->bkgView.alpha = 1.0;
    } completion:^(BOOL finished){
            [UIView animateWithDuration:0.5 delay:3 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
                self->bkgView.alpha = 0;
            } completion:^(BOOL finished){}];
    }];
}

It loads three buttons. If you click "show", it fades in a rectangle. If you click "flash" it fades in a rectangle for 3 seconds then fades it out. My problem is if you click "both" which might not make much sense in this trivial example, but simulates calling the show and flash actions consecutively which can happen in my original code. I expected it to behave the same as "flash" (as it should just take over from the "show" function immediately), but it does nothing, the rectangle is not shown.

Upvotes: 0

Views: 42

Answers (1)

DonMag
DonMag

Reputation: 77433

OK - couple things are going on...

Try this code - modified slightly from your example. I've changed the "fade in" duration to 1.5 seconds and the "fade out" duration to 2.5 seconds (can be changed at the end of viewDidLoad) so we can see a bit more of what's going on. I also added some NSLog() statements to follow the flow and print the timing information:

#import "SequentialAnimViewController.h"

@interface SequentialAnimViewController () {
    UIView *bkgView;
    double dur1;
    double dur2;
    NSDate *start;
}
@end

@implementation SequentialAnimViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    CGFloat x = 20;;
    for (NSString *str in @[@"show", @"flash", @"both"]) {
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        [button addTarget:self action:NSSelectorFromString(str) forControlEvents:UIControlEventTouchUpInside];
        [button setTitle:str forState:UIControlStateNormal];
        button.backgroundColor = [UIColor grayColor];
        button.frame = CGRectMake(x, 100, 80, 40);
        [self.view addSubview:button];
        x += 100;
    }
    bkgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 100)];
    bkgView.backgroundColor = [UIColor blackColor];
    bkgView.alpha = 0;
    [self.view addSubview:bkgView];
    
    dur1 = 1.5; //0.3;
    dur2 = 2.5; //0.5;
    
    start = [NSDate date];
}

-(void)both {
    [self show];
    [self flash];
}

-(void)show {
    [UIView animateWithDuration:dur1 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
        NSLog(@"anim A started.... \t %f", [[NSDate date] timeIntervalSinceDate:self->start]);
        self->bkgView.alpha = 1.0;
    } completion:^(BOOL finished){
        NSLog(@"anim A finished: %d \t %f", finished, [[NSDate date] timeIntervalSinceDate:self->start]);
    }];
}

-(void)flash {
    [UIView animateWithDuration:dur1 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
        NSLog(@"anim B started.... \t %f", [[NSDate date] timeIntervalSinceDate:self->start]);
        self->bkgView.alpha = 1.0;
    } completion:^(BOOL finished){
        NSLog(@"anim B finished: %d \t %f", finished, [[NSDate date] timeIntervalSinceDate:self->start]);
        [UIView animateWithDuration:self->dur2 delay:3 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
            NSLog(@"anim C started.... \t %f", [[NSDate date] timeIntervalSinceDate:self->start]);
            self->bkgView.alpha = 0;
        } completion:^(BOOL finished){
            NSLog(@"anim C finished: %d \t %f", finished, [[NSDate date] timeIntervalSinceDate:self->start]);
        }];
    }];
}

@end

First thing to try is tapping "Flash". You should see what you expect to see, with debug console output along these lines:

anim B started....   2.780529
anim B finished: 1   4.282427
anim C started....   4.282540
anim C finished: 1   9.784028

"anim B started" is inconsequential, because it's the elapsed time between viewDidLoad and when you tap "Flash"

"anim B finished: 1" shows completion == true and the elapsed time is about 1.5 seconds from "B started"

"anim C started" will occur almost instantaneously after B finishes

Now, the elapsed time for "anim C finished: 1" (completion == true) may surprise you, as it's 5.5 seconds for a 2.5 second animation. That's because you get into the anim block before the delay happens. So,

3-second delay + 2.5-second anim == 5.5

This is our first clue why your "Both" doesn't do what you think it should.

Next thing to try is - re-launch the app for clarity - and tap "Show" ... the debug output will be:

anim A started....   2.039016
anim A finished: 1   3.541033

Again, "started" is elapsed time between viewDidLoad and when you tap "Show", and "finished: 1" (completion == true), with an elapsed time of 1.5 seconds.

Now, though, while the black view is still showing, tap "Show" again:

anim A started....   2.039016
anim A finished: 1   3.541033
anim A started....   7.357023
anim A finished: 1   7.357585

Wait... 1.5 second anim only took 0.000562 seconds???

That's because black view's alpha was already == 1 ... so animating from 1.0 to 1.0 doesn't take any time.

A key point to note here: the animation does not take into account the condition of the property being animated. It evaluates the property itself.

So, what's happening when we tap "Both"? Restart the app and tap the button.

Here's my debug output:

anim A started....   1.744193
anim B started....   1.744692
anim B finished: 1   1.745148
anim C started....   1.745233
anim A finished: 0   1.745463
anim C finished: 1   7.246692

Again, "started" is elapsed time between viewDidLoad and when you tap "Both"...

Then we have elapsed times of:

0.0004989
0.000456
0.000085
0.000229
5.501229

Here's what happened:

  • on tap, start animating from alpha == 0 to alpha == 1
  • immediately start animating from alpha == 1 to alpha == 1 ... no time required, and no actual animation occurs
  • finish that animation
  • immediately start animating from alpha == 1 to alpha == 0 (with 3-second delay)
  • first anim finishes NOT complete
  • final animation runs, but we don't see animation because the 0-to-1 animation never finished

What you need to remember is that the property gets set immediately... it doesn't wait for the visual animation to complete.

Hope that's not too much information to blur the understanding of what's going on.


Edit

As to UIViewAnimationOptionBeginFromCurrentState, try this...

Change the duration to 5 seconds:

dur1 = 5.0; //0.3;

and tap "Show" ... it will be a very slow "fade in" ... after 2 seconds, tap "Show" again. It will continue the animation.

Change your show method to:

-(void)show {
    double f = bkgView.alpha == 0.0 ? 1.0 : 0.0;
    
    [UIView animateWithDuration:dur1 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void){
        NSLog(@"anim A started.... \t %f", [[NSDate date] timeIntervalSinceDate:self->start]);
        self->bkgView.alpha = f;
    } completion:^(BOOL finished){
        NSLog(@"anim A finished: %d \t %f", finished, [[NSDate date] timeIntervalSinceDate:self->start]);
    }];
}

And you can tap show, watch it slowly fade-in, tap show again in the middle of the fade, and watch it slowly fade-out from the point it was at.

The difference is (mainly) due to your "Both" approach where you immediately call the next animation.

Upvotes: 1

Related Questions