CABasicAnimation stop animation on completion - iOS - ios

I have an iOS app which is using a CABasicAnimation on repeat:
CABasicAnimation *fadeAnim = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat:0.2];
fadeAnim.duration = 1.0;
fadeAnim.autoreverses = YES;
fadeAnim.repeatCount = INFINITY;
[colourbutton.titleLabel.layer addAnimation:fadeAnim forKey:#"opacity"];
I have a button which when pressed is meant to stop the animation.
-(IBAction)stopAnim {
[colourbutton.titleLabel.layer removeAllAnimations];
}
It works fine but one thing I am noticing is that is stops the animation suddenly, it doesn't let the animation finish. So how can I get it to finish the current animation and then stop. (Or in other words how can I get it to removeAllAnimations....withAnimation?).
On a side note, do I need to include CoreAnimation framework for this to work. So far the animation is running and I havn't imported the CoreAnimation framework.
Thanks, Dan.

Just add another animation and after that remove the first one like this:
CABasicAnimation *endAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
endAnimation.fromValue = #(((CALayer *)colourbutton.titleLabel.layer.presentationLayer).opacity);
endAnimation.toValue = #(1);
endAnimation.duration = 1.0;
[colourbutton.titleLabel.layer addAnimation:endAnimation forKey:#"end"];
[colourbutton.titleLabel.layer removeAnimationForKey:#"opacity"];
The key here is to use the presentation layer to get the current state. Don't forget to set the actual end state of the layer, because the animation will be removed on completion.

In NKorotov's answer, he uses the presentationLayer to find out where you are in the animation. That is the correct way to go.
You could go with this solution, although IMO you would also have to calculate the duration animation correctly (based on the duration of the original animation and on how far you are along the animation path currently).
If you find it "silly" to add a new animation, you could perhaps call removeAllAnimations using dispatch_after at the correct time.

Related

How to properly set the model when doing CABeginAnimation

I have a UIImageView that when the user taps it, a border of 4 points toggles on and off. I'm trying to animate the border in and out as follows:
CABasicAnimation *widthAnimation = [CABasicAnimation animationWithKeyPath:#"borderWidth"];
widthAnimation.toValue = self.isSelected ? #4.0 : #0.0;
widthAnimation.duration = 0.1;
[self.imageView.layer addAnimation:widthAnimation forKey:#"borderWidth"];
Now, as I've learned from research and scouring SO, CABasicAnimation just changes the presentation layer, but not the actual model. I've also read that using fillMode and removedOnCompletion is bad practice, since it leads to inconsistencies between the model and what the user sees. So, I tried to change the model with the following line:
self.imageView.layer.borderWidth = self.isSelected ? 4.0 : 0.0;
The problem is, this line seems to set the property straight away, so by the time the animation kicks in, the border width is already at it's desired value. I've tried sticking this line at the beginning of the code, end, and everywhere in between, but to no success. I did manage to find a hacky solution: instead of setting the property, I passed the property setter to performSelector: withObject: afterDelay:, with the delay being the duration of the animation. This works most of the time, but sometimes the cycles don't quite match up, and the animation will run first, then it jumps back to the original state, then it snaps to the new state, presumably as a result of performSelector
So is there any way to smoothly animate a border without performSelector?
Any help is greatly appreciated.
Here is an example of CABasicAnimation I made a while ago :
-(void) animateProgressFrom:(CGFloat)fromValue to:(CGFloat)toValue
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = #(fromValue);
animation.toValue = #(toValue);
animation.duration = ABS(toValue - fromValue)*3.0;
[self.layer addAnimation:animation forKey:#"opacity"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layer.opacity = toValue;
[CATransaction commit];
}
I think what you needed is the CATransaction at the end of the layer animation.

how to reset/restart an animation and have it appear continuous?

So, I am fairly new to iOS programming, and have inherited a project from a former coworker. We are building an app that contains a gauge UI. When data comes in, we want to smoothly rotate our "layer" (which is a needle image) from the current angle to a new target angle. Here is what we have, which worked well with slow data:
-(void) MoveNeedleToAngle:(float) target
{
static float old_Value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation");
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_Value = target;
}
The problem is we have a new data scheme in which new data can come in (and the above method called) faster, before the animation is complete. What's happening I think is that the animation is restarted from the old target to the new target, which makes it very jumpy.
So I was wondering how to modify the above function to add a continuous/restartable behavior, as follows:
Check if the current animation is in progress and
If so, figure out where the current animation angle is, and then
Cancel the current and start a new animation from the current rotation to the new target rotation
Is it possible to build that behavior into the above function?
Thanks. Sorry if the question seems uninformed, I have studied and understand the above objects/methods, but am not an expert.
Yes you can do this using your existing method, if you add this bit of magic:
- (void)removeAnimationsFromView:(UIView*)view {
CALayer *layer = view.layer.presentationLayer;
CGAffineTransform transform = layer.affineTransform;
[layer removeAllAnimations];
view.transform = transform;
}
The presentation layer encapsulates the actual state of the animation. The view itself doesn't carry the animation state properties, basically when you set an animation end state, the view acquires that state as soon as you trigger the animation. It is the presentation layer that you 'see' during the animation.
This method captures the state of the presentation layer at the exact moment you cancel the animation, and then applies that state to the view.
Now you can use this method in your animation method, which will look something like this:
-(void) MoveNeedleToAngle:(float) target{
[self removeAnimationsFromView:imageView_Needle];
id rotation = [imageView_Needle valueForKeyPath:#"layer.transform.rotation.z"];
CGFloat old_value = [rotation floatValue]*57.2958;
// static float old_value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_value = target;
}
(I have made minimal changes to your method: there are a few coding style changes i would also make, but they are not relevant to your problem)
By the way, I suggest you feed your method in radians, not degrees, that will mean you can remove those 57.2958 constants.
You can get the current rotation from presentation layer and just set the toValue angle. No need to keep old_value
-(void) MoveNeedleToAngle:(float) targetRadians{
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.duration=5.0;
animation.fillMode=kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.removedOnCompletion=NO;
animation.fromValue = [NSNumber numberWithFloat: [[layer.presentationLayer valueForKeyPath: #"transform.rotation"] floatValue]];
animation.toValue = [NSNumber numberWithFloat:targetRadians];
// layer.transform= CATransform3DMakeRotation(rads, 0, 0, 1);
[layer addAnimation:animation forKey:#"rotate"];
}
Another way i found (commented line above) is instead of using fromValue and toValue just set the layer transform. This will produce the same animation but the presentationLayer and the model will be in sync.

Stop spinning image in place, using CABasicAnimation

I have an UIImageView spinning using the following code:
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0];
rotation.toValue = [NSNumber numberWithFloat:(2*M_PI)];
rotation.duration = 2.0;
rotation.repeatCount = HUGE_VALF;
[myView.layer addAnimation:rotation forKey:#"Spin"];
And I can stop it by running
[myView.layer removeAnimationForKey:#"Spin"];
However, when the animation stops, the image resets to where it started. This looks a little awkward if the image is mid-turn, so I would like for it to stop exactly where it is in the rotation. I assume this would require getting the current rotation amount just before stopping the animation, and then setting it back to that rotation amount instantly after stopping the animation. I do not know how to do this though.
There a couple ways of doing this. One way is my affecting the layers speed property which you can find an example of in the answer to this post: Is there a way to pause a CABasicAnimation?. But this way is more of a means to pause the animation.
Since you want to stop it, I recommend you keep calling removeAnimationForKey: like you are, and coupling it with a CATransform3D made to match the view's presentationLayer's transform before it stopped animating. Give it a try:
CATransform3D myTransform = [(CALayer*)[myView.layer presentationLayer] transform];
[myView.layer removeAnimationForKey:#"Spin"];
[myView.layer setTransform:myTransform];

Animating CAEmitterLayer's emitterPosition

I am trying to animate a CAEmitterLayer's emitterPosition like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"emitterPosition.x"] ;
animation.toValue = (id) toValue ;
animation.removedOnCompletion = NO ;
animation.duration = self.translationDuration ;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] ;
animation.completion = ^(BOOL finished)
{
[self animateToOtherSide] ;
} ;
[_emitterLayer addAnimation:animation forKey:#"emitterPosition"] ;
CGPoint newEmitterPosition = CGPointMake(toValue.floatValue, self.bounds.size.height/2.0) ;
_emitterLayer.emitterPosition = newEmitterPosition ;
Note that animation.completion is declared in a category that just calls the corresponding CAAnimation delegate method.
The problem is that this doesn't animate at all and shows the emitter in its final position. I was under the impression that once you add the animation to the layer, you should change the actual model behind it to its final position so that when the animation completes the model is in its final state; i.e., to prevent the animation from "snapping back" to its original position.
I have tried placing the last two lines in the animation.completion block, and this does indeed animate as expected. However, when the animation finishes some particles are intermittently emitted at the emitter's original position. If you put the system under load (for example, scrolling a tableview while the animation is playing), this happens more often.
Another solution I was thinking about is to not move the emitterPosition at all but just move the CAEmitterLayer itself, although I haven't tried that yet.
Thanks in advance for any help you can provide.
Perhaps emitterPosition.x is not a valid key path for animation. Try using emitterPosition instead (and so you'll have to provide CGPoint values wrapped up in an NSValue).
I just tried this on my own machine and it works fine:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:CGPointMake(30,100)];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(200,100)];
ba.duration = 6;
ba.autoreverses = YES;
ba.repeatCount = HUGE_VALF;
[emit addAnimation:ba forKey:nil];
Other things to think about:
You can typically use nil as the key in addAnimation:forKey:, unless you're going to need to find this animation later (e.g. to remove it or override it in some way). The key path is the important thing.
Setting removedOnCompletion to NO is almost always wrong and is typically the last refuge of a scoundrel (i.e. due to not understanding how animation works).
If, as you say, setting _emitterLayer.emitterPosition = newEmitterPosition inside the completion block does animate, then when why are you using CABasicAnimation at all? Why not just call UIView animate... and set the emitterPosition in the animations block? If that works, it will kill two birds with one stone, moving the position and animating it too.

How to make scaling to persist with CABasicAnimation

I am creating some animation on my application and the code below zooms out an object till it disappears. I can't figure out how to make the object to disappear and keep that way, ie. how to make the animation stay put after it finishes. Any gotchas on that? Cheers!
CABasicAnimation* zoomOut = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
zoomOut.duration = 1;
zoomOut.toValue = [NSNumber numberWithFloat:0];
[draggedObject addAnimation:zoomOut forKey:nil];
I found it. It also needs the two methods below:
zoomOut.removedOnCompletion = NO;
zoomOut.fillMode = kCAFillModeForwards;
Ok so this happens because the animation doesn't actually change the underlying property, which is why it jumps back after the animation is complete.
Try adding this line before the line starting the animation -
zoomOut.removedOnCompletion = NO;

Resources