How to make UI object responsive after CABasicAnimation - ios

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):
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];

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

Related

Why do docs indicate CALayer animations must be in UIView animation blocks?

I am currently reading Apple's Core Animation Guide, where I found the following passage regarding layer-backed views in iOS:
If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated.
Just below the quote, the documentation includes the following code listing:
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
}];
which implies that both implicit and explicit animations on CALayers backing UIViews must be placed within an animation block.
However, I have found this to be patently untrue. Specifically, I have successfully implemented explicit animations using Core Animation classes outside of a UIView animation block.
Have I misunderstood this passage, or is it out-of-date, or something else?
Some additional notes:
I assume that "the UIView class disables layer animations by default but reenables them inside animation blocks" refers to the +[UIView setAnimationsEnabled:] method. When I get back to a computer that can do so, I'll check to see whether +[UIView areAnimationsEnabled] returns YES or NO.
That quote refers to the layer that is backing the view. It is not true for stand-alone layers that you create and manage yourself.
Every view on iOS is backed by a layer. When you change the view's properties, it changes the underlying layer property. By default the layer would have implicit animations, but the layer "disables" that behavior except for when you are inside of an UIView animation block. This is what that part of the documentation is referring to.
There are a couple of ways you can use Core Animation to animate a layer property. The most common is to add the animation object to the layer when you want to animate the property, but you can also make customizations through the actions dictionary and the styles dictionary if you always want to animate when a property changes. The last two would also be disabled for the layer that is backing a view.

How to implement a D-Pad Xcode 5 on an iPad

I have tried many times to implement a D-Pad for my new maze game, but am having trouble doing so. I am doing it in the style of 4 UIButtons and if you press one, it moves another image up, down, left or right. I have tried using the quartzcore and CAAnimation, but only know limited code.
I have the methods and the buttons declared, but cannot code a working one.
I was doing:
-(IBAction)Up:(id)sender{
CGPoint origin1 = self.Player.center;
CGPoint target1 = CGPointMake(self.Player.center.x, self.Player.center.y-124);
CABasicAnimation *bounce1 = [CABasicAnimation animationWithKeyPath:#"position.y"];
bounce1.duration = 0.1;
bounce1.fromValue = [NSNumber numberWithInt:origin1.y];
bounce1.toValue = [NSNumber numberWithInt:target1.y];
[self.Player.layer addAnimation:bounce1 forKey:#"position"];
}
The ghost moves down, but bops back up immediately. I've been stumped for hours and it may be glaring me in the face, but please understand my noobishness.
In Core Animation, there are two layer hierarchies: one of model layers and one of presentation layers. Presentation layers are what you see; model layers are what you often interact with in code. This separation is valuable because it lets you create implicit animation -- e.g. set the position of a layer, and it'll animate to the new spot. (But if you assign a value to position, you want that to be the value you read back immediately thereafter, even if the animation is still in progress.)
When you use addAnimation:forKey:, you're affecting the presentation, not the model. So, during the animation, you see the Player layer moving, but that layer is "really" still where you left it. As soon as the animation ends, it's removed from the layer, so the presentation matches the model again -- you see it at its original position.
If you want the model position to change as well, you need to change it separately:
self.player.layer.position = CGPointMake(self.player.layer.position.x, target1.y);
You can either update the model immediately after adding the animation (you'll see the change after the animation completes), or use a completion block to schedule it at the end of the animation. There are subtleties to consider for either approach.
Often, though, if you just want to animate a specific change like that, especially for the main layer of a UIView, it's simpler to use the implicit animation API on UIView:
[UIView animateWithDuration:0.1 animations: ^{
self.player.center = CGPointMake(self.player.center.x, target1.y);
}];
This will both update the model value and create and run an animation for moving from the current position to the target position.
BTW: It helps to follow Cocoa code style conventions: use initial caps only for class names or other type names, and lowercase for variable/property/method names.

Changing color on screen depending on ascending value

lets say I have value in the background of my which is permanently changing. I want it to change the background color with a nice fading.
This is what I'm currently doing:
I have an Timer which reads the value every second and then chooses the new color and fades it via CAAnimation like this:
CABasicAnimation* fade = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
fade.fromValue = (id)self.view.backgroundColor.CGColor;
[self.view setBackgroundColor:changeColor];
fade.toValue = (id)changeColor.CGColor;
[fade setDuration:0.5];
[self.view.layer addAnimation:fade forKey:#"fadeAnimation"];
This works in fact ok because the animation is shorter than the actual timer so it looks alright but a bit sloppy.
How can I do this permanently and smooth without a timer?
I would suggest using UIView animations. You could use a method like animateKeyframesWithDuration:delay:options:animations:completion:, to animate a single color transition, and in the completion block trigger the next color change. In order to avoid nesting animation blocks many levels deep in completion methods, I sometimes have a method that uses an instance variable to keep track of a step counter, and the completion block just increments the step counter and calls the method again.
If you can limit your app to iOS 7, there is a new keyframe animation that you could use to animate through a whole series of colors.
There is a demo project from ShinobiControls that does exactly what you want. Check this link:
http://www.shinobicontrols.com/blog/posts/2013/10/04/ios7-day-by-day-day-11-uiview-key-frame-animations

Is it possible to control a CAAnimation's timeline?

I have a complex animation that usually runs just by itself, driven by a certain CAMediaTimingFunction. This works fine.
Now, I want to control that same animation's time(line) using an external value, for example from a slider or a gesture recognizer. In other words, I don't want to have the "clock" drive the timeline, but a slider, so one can scrub back and forth with it and "freeze" the animation by putting the slider to a certain value.
Is this possible? If so, how?
It's possible (and quite easy), but I only tried this as an experiment (for a complex animation driven by a pinch gesture recognizer), so I'd love to hear if this solution is sufficient:
You need to set the speed of your animation to 0 and the time offset to the point in time you want to jump to, e.g.
CABasicAnimation* animation = [CABasicAnimation ...];
animation.speed = 0;
animation.duration = 1;
animation.timeOffset = 0.5;
will make the animation jump to the point it would be after half a second.
Now, you can't manipulate CAAnimation objects after they've been added to the layer, so you'll need to add a new animation every time the offset changes (and remove the old one, don't forget ;).

How can I get a core animation Explicit animation to behave in exactly the same way as an Implicit animation?

I wish to do some custom animation using core animation but am having the (no doubt) classic problem of animations resetting when they have completed.
Of course this can be stopped by using:
[animation setFillMode:kCAFillModeForwards];
[animation setRemovedOnCompletion: YES];
However, I am then left with a layer that won't respond to requests to move it as an animation remains in place.
I could remove the animation, but then the position is lost... plus it all seems too complicated for something that should be achievable simply...
What I would like is that when the animation is complete, the animation leaves the layer where it left it, and then disappears as if it was never even there.
The same behaviour as an implicit animation. How can this be achieved?
At the moment I'm having to set the animation delegate and listen out for animationDidStop:finished: calls, where I then have to disable implicit animations, set the value/s from the presentation layer, and commit the "animation".
There has to be an easier way...
You might want to have a look at UIView displays improperly after its layer has been animated, your problem seems very similar.

Resources