Unexpected behavior of UIViewAnimationOptionBeginFromCurrentState with basic animation

I am trying to execute this basic UIView animation after receiving a button click:

 - (IBAction)buttonPress:(id)sender { self.sampleView.alpha = 0.0; [UIView animateWithDuration:2.0 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState animations:^{self.sampleView.alpha = 1.0;} completion:NULL]; } 

Before clicking the button, the view will be visible and has an alpha value of 1.0. When I submit a button action, I expect the view to fade from alpha = 0.0 to alpha 1.0 within 2 seconds, but that won't happen. When I UIViewAnimationOptionBeginFromCurrentState , the animation works fine.

It looks like with the UIViewAnimationOptionBeginFromCurrentState option, alpha = 0.0 is not set, and the animation is skipped because it thinks it is already 1.0.

I am trying to understand why this is happening, as the Apple documentation states that the UIViewAnimationOptionBeginFromCurrentState does not work unless another animation is executed:

" UIViewAnimationOptionBeginFromCurrentState Start the animation with the current setting associated with an already in-flight animation. If this key is not there, any animation in flight can be completed before a new animation begins. If another animation is not in flight, this key does not work."

+6
source share
3 answers

It turns out that using UIViewAnimationOptionBeginFromCurrentState does not always work as UIViewAnimationOptionBeginFromCurrentState .

Take a look at this example:

 - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; UIButton *theButton = [UIButton new]; [self.view addSubview:theButton]; theButton.frame = self.view.frame; theButton.backgroundColor = [UIColor redColor]; theButton.alpha = 0.05; [theButton addTarget:self action:@selector(actionPressed:) forControlEvents:UIControlEventTouchUpInside]; } - (void)actionPressed:(UIButton *)theButton { theButton.alpha = 0.6; [UIView animateWithDuration:5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^ { // you expect to animate from 0.6 to 1. but it animates from 0.05 to 1 theButton.alpha = 1; } completion:nil]; } 

In the above example, you expect .alpha to animate from 0.6 to 1. However, it animates from 0.05 to 1.

To solve the problem, you should change actionPressed: to the following:

 - (void)actionPressed:(UIButton *)theButton { [UIView animateWithDuration:0 animations:^ { // set new values INSIDE of this block, so that changes are // captured in UIViewAnimationOptionBeginFromCurrentState. theButton.alpha = 0.6; } completion:^(BOOL finished) { [UIView animateWithDuration:5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^ { // now it really animates from 0.6 to 1 theButton.alpha = 1; } completion:nil]; }]; } 

Mention animateWithDuration:0 !!!

The rule is easy: use only an animation block with UIViewAnimationOptionBeginFromCurrentState AFTER some other animation block, so that all your previous changes are actually applied.

If you don’t know what exactly should be included in the animateWithDuration:0 block, you can use this trick:

 - (void)actionPressed:(UIButton *)theButton { // make all your changes outside of the animation block theButton.alpha = 0.6; // create a fake view and add some animation to it. UIView *theFakeView = [UIView new]; theFakeView.alpha = 1; [UIView animateWithDuration:0 animations:^ { // we need this line so that all previous changes are ACTUALLY applied theFakeView.alpha = 0; } completion:^(BOOL finished) { [UIView animateWithDuration:5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^ { // now it really animates from 0.6 to 1 theButton.alpha = 1; } completion:nil]; }]; } 

If you do not want to recall all the details of this error, simply apply the Swizzling method to the UIView class.

IMPORTANT CHANGE: It turned out that the code below will lead to crash at runtime when you call performBatchUpdates:completion: a UICollectionView instance. Therefore, I do not recommend using method swizzling in this case!

Your code might look like this:

 + (void)load { static dispatch_once_t theOnceToken; dispatch_once(&theOnceToken, ^ { Class theClass = object_getClass(self); SEL theOriginalSelector = @selector(animateWithDuration:delay:options:animations:completion:); SEL theSwizzledSelector = @selector(swizzled_animateWithDuration:delay:options:animations:completion:); Method theOriginalMethod = class_getClassMethod(theClass, theOriginalSelector); Method theSwizzledMethod = class_getClassMethod(theClass, theSwizzledSelector); if (!theClass ||!theOriginalSelector || !theSwizzledSelector || !theOriginalMethod || !theSwizzledMethod) { abort(); } BOOL didAddMethod = class_addMethod(theClass, theOriginalSelector, method_getImplementation(theSwizzledMethod), method_getTypeEncoding(theSwizzledMethod)); if (didAddMethod) { class_replaceMethod(theClass, theSwizzledSelector, method_getImplementation(theOriginalMethod), method_getTypeEncoding(theOriginalMethod)); } else { method_exchangeImplementations(theOriginalMethod, theSwizzledMethod); } }); } + (void)swizzled_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL))completion { if (options & UIViewAnimationOptionBeginFromCurrentState) { UIView *theView = [UIView new]; theView.alpha = 1; [UIView animateWithDuration:0 animations:^ { theView.alpha = 0; } completion:^(BOOL finished) { [self swizzled_animateWithDuration:duration delay:delay options:options animations:animations completion:completion]; }]; } else { [self swizzled_animateWithDuration:duration delay:delay options:options animations:animations completion:completion]; } } 

If you add this code to your custom UIView category, then this code will work fine:

 - (void)actionPressed:(UIButton *)theButton { theButton.alpha = 0.6; [UIView animateWithDuration:5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^ { // you expect to animate from 0.6 to 1. // it will do so ONLY if you add the above code sample to your project. theButton.alpha = 1; } completion:nil]; } 

In my case, this error completely destroyed my animations. See below:

[ Demo CountPages alpha ] [ Demo CountPages alpha ]

+3
source

To find out what's going on, it’s useful to animate the position of the frame rather than alpha. If you do, try to press a button every second or so with UIViewAnimationOptionBeginFromCurrentState , and you will begin to see some kind of movement.

When you set alpha to zero, this does not update the display until the next one goes through the run loop. Since you start the animation right away, the display does not have the opportunity to update, and when the animation environment starts, it looks at the currently displayed alpha (1.0) and looks where it should go (alpha 1.0) and the interpolation between them does nothing.

If you need to save the UIViewAnimationOptionBeginFromCurrentState parameter there, because this button can be enabled when the animation on this view is already running, you can do this:

 - (IBAction)buttonPressed:(id)sender { self.sampleView.alpha = 0.0; [self performSelector:@selector(animateAlpha) withObject:nil afterDelay:0.1]; } - (void)animateAlpha { [UIView animateWithDuration:2.0 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState animations:^{self.sampleView.alpha = 1.0;} completion:NULL]; } 
+1
source

When using UIViewAnimationOptionBeginFromCurrentState instead of performSelector: I like to set initial values ​​using the UIView performWithoutAnimation right before calling animateWithDuration:

0
source

All Articles