UIViewAnimationOptionBeginFromCurrentState unexpected behaviour with basic animation
cocoa-touch, ios, objective-c, uiviewanimation
Solution
It turns out that using `UIViewAnimationOptionBeginFromCurrentState` doesn't always work as expected.
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 example above you expect that `.alpha` would animate from 0.6 to 1. However, it animates from 0.05 to 1.
In order to resolve the issue, 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 the `animateWithDuration:0`!!!
The rule is easy: Only use an animation block with `UIViewAnimationOptionBeginFromCurrentState` AFTER some other animation block, so that all your previous changes are actually applied.
In case you don't know what exactly should be included in `animateWithDuration:0` block, then 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 don't want to remember about all details of this bug, then just apply Method Swizzling to UIView class.
IMPORTANT EDIT: It turns out that the code below will cause a crash at runtime, if calling `performBatchUpdates:completion:` of `UICollectionView` instance. So, I do not recommend using `method swizzling` in this case!
Your code may 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 would work fine now:
- (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 bug has completely ruined my animations. See below:
[] []
Problem
I am trying to perform this basic `UIView` animation upon 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]; } ``` Prior to the button click the view is visible and has an alpha value of 1.0. When I send the button action I expect the view to fade in from alpha=0.0 to alpha=1.0 over 2 seconds but this does not happen. When I remove the `UIViewAnimationOptionBeginFromCurrentState` the animation works fine. It seems like with the `UIViewAnimationOptionBeginFromCurrentState` option set, the alpha=0.0 is not being set and the animation is skipped because it thinks it is already at 1.0. I am trying to understand why this would be happening since Apple documentation states that the `UIViewAnimationOptionBeginFromCurrentState` has no effect if another animation is not in progress: "`UIViewAnimationOptionBeginFromCurrentState` Start the animation from the current setting associated with an already in-flight animation. If this key is not present, any in-flight animations are allowed to finish before the new animation is started. If another animation is not in flight, this key has no effect."