Search code examples
ioscore-animationnslayoutconstraint

Animation with constraints completes instantly


I have some animation I'm trying to convert over to constraints. The idea behind it is that a login prompt bounces when there's an error.

My approach is that everything on the view controller is within bounceView, which is itself within the main view.

bounceView is tied to the main view using centering constraints and a leading space constraint. To make the layout unambiguous at design time, there's also a leading space and top space constraint. These are just placeholders, though, and are replaced with equal height/width constraints at runtime.

No other constraints are tied to the main view.

Now, some details…

I use these constraints to define the bounce:

enum {animationDone, bounceStart, bounceLeft, bounceRight, bounceReturn};
static const NSTimeInterval BounceDuration = 20.0;
static const int NumberOfBounces = 3;
static const CGFloat BounceDistance = 16.0f;

(The 20 second delay is usually much shorter; I ramped it to prove this wasn't working.)

The extra runtime constraints are set up like this:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:_bounceView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:_bounceView attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0]];
}

I start the animation with this code:

_animateCount = NumberOfBounces;
_animateStage = bounceStart;
[self animationStep];

The actual code is pretty straightforward:

- (void)animationStep {
    CGFloat bounceTo = 0;
    BOOL half = NO;
    switch (_animateStage) {
        case bounceStart:
            _animateStage = bounceLeft;
            bounceTo = -BounceDistance;
            half = YES;
            break;
        case bounceLeft:
            _animateStage = bounceRight;
            bounceTo = BounceDistance;
            break;
        case bounceRight:
            if ( --_animateCount > 0 ) {
                _animateStage = bounceLeft;
                bounceTo = -BounceDistance;
            } else {
                _animateStage = bounceReturn;
                bounceTo = 0;
            }
            break;
        case bounceReturn:
            half = YES;
            _animateStage = animationDone;
            break;
    }

    BOOL finishedAnimation = ( ( _animateStage == animationDone ) || ( self.view == nil ) );

    if ( !finishedAnimation ) {
        [_bounceView layoutIfNeeded];
        _centerX.constant = bounceTo;
        [_bounceView setNeedsUpdateConstraints];

        NSTimeInterval duration = half ? BounceDuration / 2.0f : BounceDuration;
        [UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
            NSLog(@"Bouncing to %f over %f seconds.", bounceTo, duration);
            [_bounceView layoutIfNeeded];
        } completion:^(BOOL finished) {
            [self animationStep];
        }];
    }
}

However, this is all completing instantly:

2013-10-29 10:59:43.852 App[9151:907] Bouncing to -16.000000 over 20.000000 seconds.
2013-10-29 10:59:43.864 App[9151:907] Bouncing to 16.000000 over 20.000000 seconds.
2013-10-29 10:59:43.865 App[9151:907] Bouncing to -16.000000 over 20.000000 seconds.
2013-10-29 10:59:43.866 App[9151:907] Bouncing to 16.000000 over 20.000000 seconds.
2013-10-29 10:59:43.866 App[9151:907] Bouncing to -16.000000 over 20.000000 seconds.
2013-10-29 10:59:43.867 App[9151:907] Bouncing to 16.000000 over 20.000000 seconds.
2013-10-29 10:59:43.867 App[9151:907] Bouncing to 0.000000 over 20.000000 seconds.

Any ideas?

Update

For posterity, this is caused by calling setNeedsUpdateConstraints and layoutIfNeeded on the child view. Despite Apple's documentation that implies otherwise, you need to call it on the superview. I've detailed this in an answer below.

However, I've accepted @nielsbot's answer instead. Despite not being the direct answer to my question, it's a better solution than fixing what I was trying to do.


Solution

  • Can't you use core animation to temporarily animate your view?

    See my answer here: https://stackoverflow.com/a/9371196/210171

    Basically, on the view you want to animate, get its layer and add an animation to it. If you do this by animating the layer's transform, you won't change the view's size or position and you won't trigger a layout cycle.

    CAKeyframeAnimation * anim = [ CAKeyframeAnimation animationWithKeyPath:@"transform" ] ;
    anim.values = @[ [ NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-5.0f, 0.0f, 0.0f) ], [ NSValue valueWithCATransform3D:CATransform3DMakeTranslation(5.0f, 0.0f, 0.0f) ] ] ;
    anim.autoreverses = YES ;
    anim.repeatCount = 2.0f ;
    anim.duration = 0.07f ;
    
    [ viewToShake.layer addAnimation:anim forKey:nil ] ;