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:
[] []