Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make UI object responsive after CABasicAnimation

I'm having trouble trying to find a way to make my UI object (UISegmentedControl) touch responsive after a CABasicAnimation slide in and bounce. How can I pull this off?

I know the UI object is in the presentation tree after the move. But I really like the setTimingFunction feature CABasicAnimation provides and I just won't be able to get such a smooth bounce using UIView animation.

Example GIF of animation (Looped):
enter image description here

Code I'm using inside viewDidLoad

CABasicAnimation *startAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
[startAnimation setFromValue:[NSNumber numberWithFloat:0]];
[startAnimation setToValue:[NSNumber numberWithFloat:slidingUpValue]];
[startAnimation setDuration:1.0];
startAnimation.fillMode = kCAFillModeForwards;
startAnimation.removedOnCompletion = NO;
[startAnimation setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.0 :0.0 :0.3 :1.8]];
[[gameTypeControl layer] addAnimation:startAnimation forKey:nil];
like image 297
Troy R Avatar asked Jan 16 '14 10:01

Troy R


1 Answers

What went wrong

The problem is these two lines of code and not understanding the side effects of using them:

startAnimation.fillMode = kCAFillModeForwards;
startAnimation.removedOnCompletion = NO;

The first line configures the animation to keep showing the end value after the animation has completed (you can see a visualization of fillMode and the other timing related properties in my little cheat sheet).

The second line configures the animation to stay attached to the layer after it finishes.

This sounds just fine at first, but is missing an essential part of Core Animation: animations added to a layer never affect the model, only the presentation. The Core Animation Programming Guide mention this on the second page of the section "Core Animation Basics":

The data and state information of a layer object is decoupled from the visual presentation of that layer’s content onscreen.

Animations happen on a separate layer called the presentation layer which is what you see on screen. If you print out the values of the animated property during the animation they don't change at all.

During the animation you have a difference between the presentation and the model but the animation is probably short and that difference will go away as soon as the animation finishes, so it doesn't really matter... unless the animation doesn't go away. Then the difference has persisted and now you have to deal with having out-of-sync data in two places.

How not to solve this issue

One could say that everything looks good, it's just that hit testing is wrong. Let's patch hit testing! Change the hit testing to use the segment control's layer's presentation layer and hit testing will work. Great, right? This would be like putting one plaster on top of another instead of solving the problem at its origin.

How to get rid of the side effects

It's really simple: don't use those lines and remove animations when they are finished. One might say that this technique (not removing an animation) has been used by Apple in slides and sample code so it's what Apple recommends, but there is a subtle detail that is easily missed:

When Apple introduced Core Animation at WWDC 2007 they used this technique to animate layers being removed (for example, fading out). Quote below is from Session 211 - Adding Core Animation to Your Application:

  • To animate layer removal, use animations with
    fillMode = forwards, removedOnCompletion = NO
    • Animation delegate can remove the layer

In this case it's perfectly fine to not remove the animation since it could cause the layer to jump back to it's original size, opacity, etc. for one frame before being removed from the layer hierarchy. As they said in the above slide: the "animation delegate can remove the layer" (that is: do the clean up).

In other cases no one does the clean up and you are left with the mess of having out of sync data in two places (as mentioned above).

The solution is a different approach to the animation

When building animations, I try to think of like this:

If your animation mysteriously went away, the model value should be the expected end state.

Applied to your example, the segmented control is moving towards it's final position to stop there and stay there. In that case the actual position of the segmented control should be the end position. The application should work even if the animation isn't there.

So what about the animation? Instead of animating from 0 offset to some offset, you reverse that and animate from some negative offset to 0 offset.

CABasicAnimation *startAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
[startAnimation setFromValue:@(-slidingUpValue)]]; // these two changed 
[startAnimation setToValue:@(0.0)];                // places with each other
[startAnimation setDuration:1.0];
// no fill mode
// animation is removed on completion
[startAnimation setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.0 :0.0 :0.3 :1.8]];
[[gameTypeControl layer] addAnimation:startAnimation forKey:@"Move in game type control from bottom"]; // I like adding descriptive keys for easier debugging
like image 123
David Rönnqvist Avatar answered Oct 27 '22 14:10

David Rönnqvist