How to specify selector when CAKeyframeAnimation is finished? - iphone

I'm using a CAKeyframeAnimation to animate a view along a CGPath. When the animation is done, I'd like to be able to call some other method to perform another action. Is there a good way to do this?
I've looked at using UIView's setAnimationDidStopSelector:, however from the docs this looks like it only applies when used within a UIView animation block (beginAnimations and commitAnimations). I also gave it a try just in case, but it doesn't seem to work.
Here's some sample code (this is within a custom UIView sub-class method):
// These have no effect since they're not in a UIView Animation Block
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop:finished:context:)];
// Set up path movement
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"path"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 1.0f;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, self.center.x, self.center.y);
// add all points to the path
for (NSValue* value in myPoints) {
CGPoint nextPoint = [value CGPointValue];
CGPathAddLineToPoint(path, NULL, nextPoint.x, nextPoint.y);
}
pathAnimation.path = path;
CGPathRelease(path);
[self.layer addAnimation:pathAnimation forKey:#"pathAnimation"];
A workaround I was considering that should work, but doesn't seem like the best way, is to use NSObject's performSelector:withObject:afterDelay:. As long as I set the delay equal to the duration of the animation, then it should be fine.
Is there a better way? Thanks!

Or you can enclose your animation with:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
/* what to do next */
}];
/* your animation code */
[CATransaction commit];
And set the completion block to handle what you need to do.

CAKeyframeAnimation is a subclass of CAAnimation. There is a delegate property in CAAnimation. The delegate can implement the -animationDidStop:finished: method. The rest should be easy.

Swift 3 syntax for this answer.
CATransaction.begin()
CATransaction.setCompletionBlock {
//Actions to be done after animation
}
//Animation Code
CATransaction.commit()

Related

iOS rotation animation does not happen smoothly in CAAnimationGroup

I have 3 animations running in a group: position, scale and rotation
However my rotation animation does not happen smoothly. It happens abruptly in the beginning, while other animations happen smoothly.
Here is my code:
//CABasicAnimation *scaleAnimation
//CABasicAnimation *moveAnimation
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
rotationAnimation.autoreverses = NO;
rotationAnimation.toValue = [NSNumber numberWithFloat: 0];//-(M_PI/3)];
//below line shows the end state, if removed, the layer will assume its initial position after animation, something which I don't want.
[layer setTransform:CATransform3DMakeRotation(0, 0, 1, 0)];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
//Code for adding all 3 animations to the group
Edit:
rotationAnimation.toValue = [NSNumber numberWithFloat: -(M_PI/3)];
Removed final transform value from the place of animation declaration, and added it to delegate. (note that in case of animation group, individual animation delegates do not get hit, only the group delegate worked).
Under delegate:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
NSString * animName = [theAnimation valueForKey:#"animationName"];
CALayer* layer = [theAnimation valueForKey:#"animationLayer"];
if ([animName isEqualToString:#"MoveZoomOut"])
{
if ([layer.name isEqualToString:#"Layer1"])
[layer setTransform:CATransform3DMakeRotation(-M_PI/3, 0, 1, 0)];
else if ([layer.name isEqualToString:#"Layer2"])
[layer setTransform:CATransform3DMakeRotation(M_PI/3, 0, 1, 0)];
else
[layer setTransform:CATransform3DMakeRotation(0, 0, 1, 0)];
}
if ([layer respondsToSelector:#selector(setContentsScale:)])
{
layer.contentsScale = [UIScreen mainScreen].scale;
}
}
I have 3 animations running in a group: position, scale and rotation
I suspect that since you are animating both scale and rotation, which is done through the transform property, there might be a clash.
Try to animate scale and rotation in a single animation by concatenating the 2 transforms into one:
CATransform3D t = CATransform3DMakeRotation(0, 0, 1, 0)];
[layer setTransform:CATransform3DScale(t, sx, sy, sz)];
EDIT:
I understand that what is happening is pretty unusual, so I will give you a couple of hints as to tricks that in several occasions helped me. I understand, you have 3 layers to animate.
wrap each layer animations in [CATransaction begin]/[CATransaction commit];
if that does not help,
do not set the final value of the property you are going to animate right after creating the animation itself; rather set it in the animation delegate animationDidStop:finished: method.
In other words, given your code:
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
rotationAnimation.autoreverses = NO;
rotationAnimation.toValue = [NSNumber numberWithFloat: 0];//-(M_PI/3)];
//below line shows the end state, if removed, the layer will assume its initial position after animation, something which I don't want.
[layer setTransform:CATransform3DMakeRotation(0, 0, 1, 0)];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
don't do [layer setTransform:CATransform3DMakeRotation(0, 0, 1, 0)]; there. Rather, do it in:
- (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag {
[layer setTransform:CATransform3DMakeRotation(0, 0, 1, 0)];
}
Since you will have more than one animation going on (and presumably use the same delegate for all of them), you will need a way to tell which animation the animationDidStop: method refers to. To this aim, you could do, e.g.:
//-- se the delegate
rotationAnimation.delegate = self;
[rotationAnimation setValue:layer forKey:#"animationLayer"];
and in the delegate method:
- (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag {
[CATransaction begin];
[CATransaction setDisableActions:YES];
CALayer* layer = [anim valueForKey:#"animationLayer"];
layer.transform = ...;
[CATransaction commit];
}
I know that this seems a bit contrived. And it is. But it is the only way I could fix an issue similar to what you are reporting.
Hope this helps.
EDIT:
For the glitch when using animationDidStop:, try to wrap animationDidStop: content in:
[CATransaction begin];
[CATransaction setDisableActions:YES];
...
[CATransaction commit];

Moving an image along a series of CGPoints

I have path stored in an array of CGPoints which I'd like to move an image along. Here's the general code I have so far:
-(void)movePic:(id)sender{
for(int i = 0; i < self.array.count; i++){
CGPoint location = [[self.array objectAtIndex:i] CGPointValue];
[UIView animateWithDuration:0.1 animations:^{
self.imageView.center = location;
} completion:^(BOOL finished){
}];
}
}
The problem is the for loop runs extremely fast, so you only see the animation on the last points. I'm unsure of how to better design this. Ideally, what could I do to make sure one animation finishes before the other begins? Should I not use a for loop? Thanks
Your code assumes that UIView animations run synchronously in the main thread, which they do not.
You seem to have two options
Explicit CAKeyframeAnimation for animating a CALayer along any amount of sample points (interpolated between them)
Implicit recursive UIView animation for animating a UIView along a series of sample points (interpolated between them)
The former would be much more efficient - still I thought I oould show you both options.
CAKeyframeAnimation
- (void)movePic:(id)sender
{
//create a mutable core-graphics path
CGMutablePathRef path = CGPathCreateMutable();
for(int i = 0; i < self.array.count; i++)
{
CGPoint location = [[self.array objectAtIndex:index] CGPointValue];
CGPathAddLineToPoint(path, nil, location.x, location.y);
}
//create a new keyframe animation
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
//add our path to it
pathAnimation.path = path;
//be nice to the system
CGPathRelease(path);
//setup some more animation parameters
pathAnimation.duration = 0.1 * self.array.count;
//add the animation to our imageView's layer (which will start the animation)
[self.imageView.layer addAnimation:pathAnimation forKey:#"pathAnimation"];
}
UIView Animation
- (void)movePicToPointAtIndex:(unsigned int)index
{
//safeguard check...
if ([self.array count] <= index)
return;
//get the next location
CGPoint location = [[self.array objectAtIndex:index] CGPointValue];
//animate the imageView center towards that location
[UIView animateWithDuration:0.1
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
animations:^{
self.imageView.center = location;
} completion:^(BOOL finished){
//we are done with that animation, now go to the next one...
[self movePicToPointAtIndex:index+1];
}];
}
- (void)movePic:(id)sender
{
[self movePicToPointAtIndex:0];
}
Ok, the thing you have to do is set the array of points as a property of the class, something like animationPath. Ok, so now you would have to pay attention to the delegate methods of the UIView animation delegate methods (it's not actually a different class, it's just a delegate of the class' methods).
Set a method to call on setAnimationDidStopSelector:selector every time the animation stops, so here you would have something like this:
//Inside the callback for setAnimationDidStopSelector
if ([animationPath count] != 0){
//Go to next point
CGPoint location = [[self.array objectAtIndex:0] CGPointValue];
[UIView animateWithDuration:0.1 animations:^{
self.imageView.center = location;
} completion:^(BOOL finished){
}];
}
else{
NSLog(#"Nowhere else to go, animation finished :D");
}
So just be sure to fire your animation with the first point.
As far as I remember UIViews animations manage things in other threads so that's probably why the for statement is not working.

Manipulating UIImageView During Animation with CAKeyframeAnimation

I need to move object (UIImageView) from point A to point B, then rotate image at point B by 180 degrees to face the opposite direction and move object from B to A (backwards).
I have the code below which moves from A to B. I also know how to rotate UIImageView. But how to know when object has reached point B and how to apply rotation action considering there are many objects on the screen?
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = speed;
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
CGMutablePathRef pointPath = CGPathCreateMutable();
CGPathMoveToPoint(pointPath, NULL, rect.origin.x, rect.origin.y);
CGPathAddLineToPoint(pointPath, NULL, x, y);
pathAnimation.path = pointPath;
CGPathRelease(pointPath);
[imageView.layer addAnimation:pathAnimation forKey:[NSString stringWithFormat:#"pathAnimation%#",objId]];
[imageView release];
Set a delegate on the CAAnimation object, and in the delegate implement the animationDidStop:finished: method.
OTOH, the animation you described sounds like it could be done easily enough with UIView's animation support. See Animating Views with Blocks if you're targeting 4.0 and up, or Animating Views if you're still targeting earlier versions.
You can use the CAAnimation delegate method
-(void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag

crossfade animation between two UIImages in objective-c doesn't fade out the first UIImage

I want to crossfade between two different UIImages but for a reason i cannot figure out my first UIImage stays when the second one fades in.
Here is the sourcecode of my animation (it does a lot more than just crossfading, but the crossfading is my only problem right now).
I need all of the three animations listed below to be executed at the same time.
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,startPoint.x, startPoint.y);
CGPathAddLineToPoint(thePath, NULL, endPoint.x, endPoint.y);
CAKeyframeAnimation *positionAnimation =[CAKeyframeAnimation animationWithKeyPath:#"position"];
positionAnimation.path=thePath;
positionAnimation.duration=ti_duration;
positionAnimation.repeatCount=0;
positionAnimation.delegate = self;
positionAnimation.autoreverses=NO;
positionAnimation.fillMode=kCAFillModeForwards;
positionAnimation.removedOnCompletion = NO;
[self.layer addAnimation:positionAnimation forKey:s_direction];
CABasicAnimation *crossFade = [CABasicAnimation animationWithKeyPath:#"contents"];
crossFade.duration = ti_duration;
crossFade.fromValue = (id)img_startImage.CGImage;
crossFade.toValue = (id)img_transferImage.CGImage;
[self.iv_image.layer addAnimation:crossFade forKey:#"animateContentsToTransferState"];
[self.iv_image setImage:img_transferImage];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theGroup.duration = ti_duration;
theGroup.repeatCount = 0;
theGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
theGroup.animations = [NSArray arrayWithObjects:/*positionAnimation,*/ crossFade, nil]; // you can add more
[self.layer addAnimation:theGroup forKey:#"move"];
[UIView beginAnimations:#"changeSize" context:nil];
[UIView setAnimationDuration:duration];
self.bounds = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.transferSize.width, self.transferSize.height);
[UIView commitAnimations];
The animation method containing this sourcecode is in a subclass of UIView. This subclass contains several UIImages and one UIImageView where the image to be displayed is contained in.
Have i forgotten some essential thing or why is my first image not fading away?
Can it be because it is animated from some other animation at the same time?
I hope someone can help me with this.
Greets
Maverick
Think of CAAnimations as operations applied to the CALayer. The animations current state do not change the layers original contents.
When the original state seems to snap back at the end of the animation it is actually just the animations operation being removed, and the real layers state is being revealed again.
What you need to do is to register a delegate for your animation, and change the actual self.layer.contents when the animation ends.
First:
crossFade.delegate = self;
Then implement:
- (void)animationDidStop:(CABasicAnimation *)animation finished:(BOOL)flag {
self.layer.contents = animation.toValue;
}
Try this code...It worked for me.
-(void)crossFadeImage{
CABasicAnimation *crossFade = [CABasicAnimation animationWithKeyPath:#"contents"];
crossFade.duration = 2.0;
crossFade.fromValue = img1.CGImage;
crossFade.toValue = img2.CGImage;
[self.background.layer addAnimation:crossFade forKey:#"animateContents"];
self.background.image = img2;
}

Core Animation problem on iPhone

I'm new to iPhone development, and doing some experimentation with Core Animation. I've run into a small problem regarding the duration of the animation I'm attempting.
Basically, Ive got a view with two subviews, and I'm trying to animate their opacity so that one fades in while the other fades out. Problem is, instead of a gradual fade in/out, the subviews simply switch instantly to/from full/zero opacity. I've tried to adjust the animation duration with CATransaction with no noticable effect. It's also not specific to animating opacity - animating position shows the same problem.
The code I'm using (inside a method of the superview) follows:
CALayer* oldLayer = ((UIView*) [[self subviews] objectAtIndex:0]).layer;
CALayer* newLayer = ((UIView*) [[self subviews] objectAtIndex:1]).layer;
[CATransaction begin];
[CATransaction setAnimationDuration:1.0f];
oldLayer.opacity = 0.0;
newLayer.opacity = 1.0;
[CATransaction commit];
Does anyone have an idea what the problem might be?
Noah's solution is the cleanest way to do what you want, but the reason why you're not seeing the animation is that implicit animations are disabled for CALayers that back UIViews. Because nothing is animating, the CATransaction is unable to set the duration for anything.
If you really wanted to perform this animation on the layer, you'd need to set up a manual CABasicAnimation to do this:
CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacityAnimation.removedOnCompletion = NO;
opacityAnimation.fillMode = kCAFillModeForwards;
[oldLayer addAnimation:opacityAnimation forKey:#"opacity"];
[newLayer addAnimation:opacityAnimation forKey:#"opacity"];
[CATransaction begin];
[CATransaction setAnimationDuration:1.0f];
oldLayer.opacity = 0.0;
newLayer.opacity = 1.0;
[CATransaction commit];
(Note that I'm not sure if you can re-use the opacity animation here. You might need to create separate instances for each layer.)
Is there a reason you're using Core Animation over the generic UIView animation wrapper? It's pretty straightforward to do something like this with
UIView *oldView = [[self subviews] objectAtIndex:0];
UIView *newView = [[self subviews] objectAtIndex:1];
[UIView beginAnimations:#"swapViews" context:nil];
[UIView setAnimationDuration:1];
oldView.alpha = 0;
newView.alpha = 1;
[UIView commitAnimations];
Do you need to use an explicit CATransaction here?
The docs here imply that you don't.