0

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.

Steven Fisher
  • 44,462
  • 20
  • 138
  • 192

3 Answers3

1

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 ] ;
Community
  • 1
  • 1
nielsbot
  • 15,922
  • 4
  • 48
  • 73
0

Is the pattern suggested in this post helpful at all?

I understand you're using the more fine-grained control approach to your animations than the simple one in that post, but I think the underlying setNeedsUpdateLayoutConstraints and subsequent layoutIfNeeded calls are still applicable here.

Community
  • 1
  • 1
ryan cumley
  • 1,901
  • 14
  • 11
  • I think that's what I'm doing: `[_bounceView setNeedsUpdateConstraints]` outside the block, then `[_bounceView layoutIfNeeded]` inside. – Steven Fisher Oct 29 '13 at 19:13
  • Yeah, I've been playing around with it for the past 20 minutes or so and getting the same behavior as you (instant transitions). I'll chime back in if I come up with something. – ryan cumley Oct 29 '13 at 19:28
0

You must call both setNeedsUpdateConstraints and layoutIfNeeded on the superview. Despite Apple's documentation saying layoutIfNeeded will crawl up the view, this doesn't seem to be the case.

For future reference, Apple's current documentation for layoutIfNeeded:

Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor. Therefore, calling this method can potentially force the layout of your entire view hierarchy.

Steven Fisher
  • 44,462
  • 20
  • 138
  • 192