Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIViewAnimationOptionBeginFromCurrentState unexpected behaviour with basic animation

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."

like image 337
Mike M Avatar asked Feb 01 '14 08:02

Mike M


2 Answers

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:

[Demo CountPages alpha] [Demo CountPages alpha]

like image 109
OlDor Avatar answered Jan 02 '23 13:01

OlDor


In order to see what's going on, it's helpful to animate the frame position rather than the alpha. If you do this, try tapping the button every second or so with UIViewAnimationOptionBeginFromCurrentState set and you will start to see some movement.

When you set the alpha to zero, this doesn't update the display until the next pass through the run loop. Since you start your animation immediately afterward, the display doesn't get a chance to update and when the animation framework starts, it looks at the current displayed alpha (1.0) and looks at where it has to get to (alpha of 1.0) and the interpolation between the two does nothing.

If you need to keep the UIViewAnimationOptionBeginFromCurrentState option in there because this button might be tapped when an animation on this view is already in progress, 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];
}
like image 44
wrightak Avatar answered Jan 02 '23 11:01

wrightak