The scope of a CATransaction block - iphone

I have seen some code that removes a given layer from it's superlayer by doing the following:
void RemoveImmediately(CALayer *layer) {
[CATransaction flush];
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
[layer removeFromSuperlayer];
[CATransaction commit];
}
I have written a method that changes the position of a given layer in an animated way using CAKeyframeAnimation, which looks like this:
- (void)animateMovingObject:(NXUIObject*)obj
fromPosition:(CGPoint)startPosition
toPosition:(CGPoint)endPosition
duration:(NSTimeInterval)duration {
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.duration = duration;
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, startPosition.x, startPosition.y);
CGPathAddCurveToPoint(curvedPath, NULL,
startPosition.x, endPosition.y,
startPosition.x, endPosition.y,
endPosition.x, endPosition.y);
pathAnimation.path = curvedPath;
[obj addAnimation:pathAnimation forKey:#"pathAnimation"];
CGPathRelease(curvedPath);
}
Up to know, everything is fine. Now suppose I have in my app a 'master layer', which has 3 sublayers. I want the first two sublayers to be moved to another position and the last one to be removed. So I did the following:
CALayer obj1 = ... // set up layer and add as sublayer
[self.masterLayer addSublayer:obj1];
[self animateMovingObject:obj1
fromPosition:CGPointMake(0.0, 0.0)
toPosition:CGPointMake(100.0, 100.0)
duration:2.0];
CALayer obj2 = ... // set up layer and add as sublayer
[self.masterLayer addSublayer:obj2];
[self animateMovingObject:obj2
fromPosition:CGPointMake(0.0, 0.0)
toPosition:CGPointMake(150.0, 100.0)
duration:2.0];
CALayer obj3 = ... // set up layer and add as sublayer
[self.masterLayer addSublayer:obj3];
// ...
RemoveImmediately(obj3); // This removes the two previous animations too
//[obj3 removeFromSuperlayer]; // This just removes obj3, the previous animations works
Note that if I call RemoveImmediately(), even passing obj3 as argument I can't see the first two layers being animated. However, if I remove the obj3 just by calling removeFromSuperlayer the first two layers are animated normally.
Looks like the CATransaction block cancels all animations being done, even those that were created with CAKeyframeAnimation.
What I'm I missing?
Thanks In advance.

The scope of a CATransaction should just be between the [CATransaction begin] and [CATransaction commit] lines.
Also, you shouldn't need the [CATransaction flush] in that code. Apple generally recommends you not force the execution of pending transactions for performance reasons, and the transaction you're setting up there should not affect any pending animations.
I'm guessing this odd behavior is still related to what you asked in your previous question, where the three animations are somehow interfering with one another. As I commented there, this is behaving like the three animations are being added to one layer, not three independent ones. I'd check and make sure there was no possibility of that happening (crossed pointers, etc.).

Related

iOS "CABasicAnimation" brings component to the original position after animation complete

I'm animating UIImageView using iOS core animation "CABasicAnimation", all works fine for me but the only problem is when i animate to the position, after completeing animation it comes back to original position where it was. how can i overcome this? i need to keep UIImageView in moved position.
NOTE : I've seen few questions with success answers regarding this, but i have no idea why mine is not working like they say.
After rotating a CALayer using CABasicAnimation the layer jumps back to it's unrotated position
Here is my sample code,
CGPoint endPt = CGPointMake(160, 53);
CABasicAnimation *anim5 = [CABasicAnimation animationWithKeyPath:#"position"];
[anim5 setBeginTime:CACurrentMediaTime()+0.4];
anim5.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
anim5.fromValue = [NSValue valueWithCGPoint:imgRef.layer.position];
anim5.toValue = [NSValue valueWithCGPoint:endPt];
anim5.duration = 1.5;
anim5.speed = 2;
[imgRef.layer addAnimation:anim5 forKey:#"position"];
//this is what other answers ask to do
[anim5 setFillMode:kCAFillModeForwards];
[anim5 setRemovedOnCompletion:NO];
BTW [imgRef.layer setPosition:CGPointMake(160, 53)]; won't help me since i'm delaying animation with 4 milliseconds.
The root cause is that the animation just transitions the property between two values, it doesn't actually change the ending value. You need to change the ending value when the animation completes, there are three ways to do that.
1) Use the delegate property on the CAAnimation superclass to be notified of when the animation completes. At that point you can set the property to it's end value. See: https://developer.apple.com/library/mac/#documentation/GraphicsImaging/Reference/CAAnimation_class/Introduction/Introduction.html#//apple_ref/occ/cl/CAAnimation The animationDidStop:finished: is the method you'll need to implement on the delegate.
2) Set a completion block on the surrounding CATransaction. You'll need to manually start the CATransaction rather than having CABasicAnimation do that automatically for you. See: Objective-C - CABasicAnimation applying changes after animation?
3) See OMZ's comment below...
The right answer is to set the layer's position property, but as you've pointed out, it makes it more difficult because you're wanting a 0.4 second delay prior to the position change. Is there any reason you couldn't perform the delay first and then do the animation? Something like this:
- (IBAction)didTapMove:(id)sender
{
[self performSelector:#selector(animate) withObject:nil afterDelay:0.4];
}
- (void)animate
{
CGPoint endPt = CGPointMake(160, 53);
CABasicAnimation *anim5 = [CABasicAnimation animationWithKeyPath:#"position"];
anim5.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
anim5.fromValue = [NSValue valueWithCGPoint:_imageView.layer.position];
anim5.toValue = [NSValue valueWithCGPoint:endPt];
anim5.duration = 1.5;
anim5.speed = 2;
[_imageView.layer addAnimation:anim5 forKey:#"position"];
[_imageView.layer setPosition:CGPointMake(160, 53)];
}
Noticed I've removed your begin time from the animation since the delay is occurring in the perform selector call.

multiple CAKeyframeAnimation in different layers at the same time

Does anyone now how can I animate multiple layers at the same time using CAKeyframeAnimation ?
Each layer has its own CAKeyframeAnimation object. Take a look at the code below:
I have a method that receives an object, creates the CAKeyframeAnimation and attaches the animation to it:
- (void)animateMovingObject:(CALayer*)obj
fromPosition:(CGPoint)startPosition
toPosition:(CGPoint)endPosition
duration:(NSTimeInterval)duration {
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
//pathAnimation.fillMode = kkCAFillModeRemoved; // default
//pathAnimation.removedOnCompletion = YES; // default
pathAnimation.duration = duration;
// create an empty mutable path
CGMutablePathRef curvedPath = CGPathCreateMutable();
// set the starting point of the path
CGPathMoveToPoint(curvedPath, NULL, startPosition.x, startPosition.y);
CGPathAddCurveToPoint(curvedPath, NULL,
startPosition.x, endPosition.y,
startPosition.x, endPosition.y,
endPosition.x, endPosition.y);
pathAnimation.path = curvedPath;
[obj addAnimation:pathAnimation forKey:#"pathAnimation"];
CGPathRelease(curvedPath);
}
Now, suppose I have 3 layers added as a sublayer in my board game and I make the following calls:
CALayer obj1 = ... // set up layer and add as sublayer
[self animateMovingObject:obj1
fromPosition:CGPointMake(0.0, 0.0)
toPosition:CGPointMake(100.0, 100.0)
duration:2.0];
CALayer obj2 = ... // set up layer and add as sublayer
[self animateMovingObject:obj2
fromPosition:CGPointMake(0.0, 0.0)
toPosition:CGPointMake(150.0, 100.0)
duration:2.0];
CALayer obj3 = ... // set up layer and add as sublayer
[self animateMovingObject:obj3
fromPosition:CGPointMake(0.0, 0.0)
toPosition:CGPointMake(200.0, 100.0)
duration:2.0];
By doing this, I can see only the obj3 being moved from position (0.0, 0.0) to (200.0, 100.0).
What am I missing? Should I use NSOperationQueue/Threads?
Using the animationDidStart: delegate method of the CAKeyframeAnimation doesn't seem to be useful in this context.
Any ideas?
Thanks in advance.
If you want to group a series of animations like this, so that they are guaranteed to be synchronized, you could wrap them in a CATransaction:
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.0] forKey:kCATransactionAnimationDuration];
// Do animations
[CATransaction commit];
However, I'm not quite sure why the code you've written doesn't fire off the correct animations for each layer, even if they weren't synchronized.
Note that by setting removedOnCompletion to YES, your animation will run and then the object will hop back to its starting point at the end. You probably want that to be NO.

CABasicAnimation not animating my property

I've been trying to understand what is wrong with my animation and I still haven't figure it out. I think it should be really straight forward, but there is probably something I'm missing, even after reading lot of examples and documentation.
My problem comes originally form the fact that on the iPhone, you cannot resize layers automatically (with the view). The documentation says otherwise but there is no autoresizeMask for the layer in the SDKs. So I decided to make a little workaround and animate the layer myself.
I've got this simple piece of code that should do a simple resize animation. The values are good and I even set the delegate in order to trace if the anim start/end.
// I've got a property named layerImage (which is a CALayer)
- (void)animateTestWithFrame:(CGRect)value {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"layerImage.frame"];
animation.duration = 1;
animation.fromValue = [NSValue valueWithCGRect:self.frame];
animation.toValue = [NSValue valueWithCGRect:value];
animation.removedOnCompletion = YES;
animation.delegate = self;
[self.layer addAnimation:animation forKey:#"layerImage.frame"];
}
So, any ideas? (This view that contains the code is the subview of a subview of the window if that could make a difference)
--- EDIT ---
It seems that frame is not animatable via CABasicAnimation and the named property "frame". When using bounds, I've got some strange result, but at least I'm getting something. Will continue investigating on this.
So it's good that you've figured things out here, but your answer to your own question has some inaccuracies. Let me correct a few things:
The frame property can be animated--just not with explicit animation. If you do the following to a layer other than the root layer of a view, the frame will animate just fine:
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
[animationLayer setFrame:CGRectMake(100.0f, 100.0f, 100.0f, 100.0f)];
[CATransaction commit];
Remember that setting a property on a layer will animate that property change by default. In fact you have to disable animations on a property change if you don't want it to animate. (Of course this is only true if you are animating a layer other than the root layer of a view.) You are correct in animating both position and bounds if you need to use an explicit animation.
You can animate the frame on a UIView using implicit animation:
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:3.0f];
[[self view] setFrame:CGRectMake(45.0f, 45.0f, 100.0f, 100.0f)];
[UIView commitAnimations];
This will animate from the view's current frame (bounds and position) to x = 45.0, y = 45.0, w = 100.0, h = 100.0.
It seems you may also be misunderstanding the difference between an animation and a layer. You add animations to layers, but adding an animation to a layer does not automatically set the property that you're animating.
CALayers are model objects. They contain information about the layer that eventually gets rendered to screen. You must set a layer's property if you want that property to actually have that value. If you simply animate the property, it will only be a visual representation and not actual--which is to say this is why the value snaps back to the original value of the layer because you never actually changed it.
Which leads me to the next point. You said:
Use "animation.removedOnCompletion =
NO; animation.fillMode =
kCAFillModeForwards;" to ensure that
the values are not reseted at the end
of the animation
This is not exactly right. These two values simply cause the animation to remain at it's final position visually, however, the layer's actual values have not changed. They are still the exact same values they were when you started the animation. In general (with a few exceptions) you don't want to use these two parameters because they are visual only. What you want is to actually set the layer value for the property you're animating.
Say, for example, that you want to animate the position of your layer using an explicit animation. Here is the code you want:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithCGPoint:CGPointMake(70.0f, 70.0f)]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(150.0f, 150.0f)]];
[animation setDuration:2.0f];
// Actually set the position on the *layer* that you want it to be
// when the animation finishes.
[animationLayer setPosition:CGPointMake(150.0f, 150.0f)];
// Add the animation for the position key to ensure that you
// override the animation for position changes with your animation.
[animationLayer addAnimation:animation forKey:#"position"];
You may also want to consider animation grouping. With an animation group, you can group several animations together and then control how they relate to each other. In your case the duration for your bounds and position animations are the same and so what you are trying to do will work fine without a group, but if you wanted to offset the start of the animations, for example you didn't want the position animation to start until a second or two into the frame animation, you could stagger them by setting the beginTime value of the position animation.
Finally, I would be curious to know why you couldn't use the implicit animations available on UIView. I use these in the vast majority of the Core Animation code I write and can't see why this wouldn't work for your situation.
Best regards.
The key path should only be the key path of the property, not the name of the object as well.
Use this
[CABasicAnimation animationWithKeyPath:#"frame"]
instead of this
[CABasicAnimation animationWithKeyPath:#"layerImage.frame"]
And just BTW, when you add animation to a layer, the key doen't mean the key property to animate. Just the key (name) that you want this animation to have (this refers to the last line your code)
So, the solution was to animate the #"bounds" and the #"position" of the layer because frame is not animatable on iPhone. It took me some time to understand that the position was the center of the layer and the resize of the bounds was extending from the center, but that was the easy part.
So, what I did in resume was:
In the setFrame, create 2 animations with the bounds and position property.
Use "animation.removedOnCompletion = NO; animation.fillMode = kCAFillModeForwards;" to ensure that the values are not reseted at the end of the animation
Register the delegate to self in order to implements "animationDidStop:finished:". It seems that you still need to set the values: "layerImage.bounds = [animation.toValue CGRectValue]; layerImage.position = [animation.toValue CGPointValue];".
I wasn't able to use the UIView animation system directly because it wasn't doing what I wanted on the layers.
Thanks tadej5553 for pointing me out the layer problem I had with the "addAnimation". So here is the code for those who would like to see what it looks like.
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
CABasicAnimation *animation = (CABasicAnimation*)anim;
if ([animation.keyPath isEqualToString:#"bounds"]) {
layerImage.bounds = [animation.toValue CGRectValue];
} else if ([animation.keyPath isEqualToString:#"position"]) {
layerImage.position = [animation.toValue CGPointValue];
}
}
- (void)setFrame:(CGRect)value {
CGRect bounds = CGRectMake(0, 0, value.size.width, value.size.height);
if ([UIView isAnimationStarted]) {
// animate the bounds
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"bounds"];
animation.duration = [UIView animationDuration];
animation.fromValue = [NSValue valueWithCGRect:layerImage.bounds];
animation.toValue = [NSValue valueWithCGRect:bounds];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [UIView animationFunction];
animation.delegate = self;
[layerImage addAnimation:animation forKey:#"BoundsAnimation"];
// animate the position so it stays at 0, 0 of the frame.
animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.duration = [UIView animationDuration];
animation.fromValue = [NSValue valueWithCGPoint:layerImage.position];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(bounds.size.width / 2, bounds.size.height / 2)];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [UIView animationFunction];
animation.delegate = self;
[layerImage addAnimation:animation forKey:#"PositionAnimation"];
} else {
layerImage.frame = bounds;
}
[super setFrame:value];
}

CATransaction: Layer Changes But Does Not Animate

I'm trying to animate part of UI in an iPad app when the user taps a button. I have this code in my action method. It works in the sense that the UI changes how I expect but it does not animate the changes. It simply immediately changes. I must be missing something:
- (IBAction)someAction:(id)sender {
UIViewController *aViewController = <# Get an existing UIViewController #>;
UIView *viewToAnimate = aViewController.view;
CALayer *layerToAnimate = viewToAnimate.layer;
[CATransaction begin];
[CATransaction setAnimationDuration:1.0f];
CATransform3D rotateTransform = CATransform3DMakeRotation(0.3, 0, 0, 1);
CATransform3D scaleTransform = CATransform3DMakeScale(0.10, 0.10, 0.10);
CATransform3D positionTransform = CATransform3DMakeTranslation(24, 423, 0);
CATransform3D combinedTransform = CATransform3DConcat(rotateTransform, scaleTransform);
combinedTransform = CATransform3DConcat(combinedTransform, positionTransform);
layerToAnimate.transform = combinedTransform;
[CATransaction commit];
// rest of method...
}
I've tried simplifying the animation to just change the opacity (for example) and it still will not animate. The opacity just changes instantly. That leads me to believe something is not setup properly.
Any clues would be helpful!
Animations on the root layer of a view are disabled by default. Try applying a transform to the view instead, e.g. [view setTransform:CGTransform3D...]. If you must do it at the layer level, add a layer to the root layer and perform your transforms on it instead. Also the view animation block [UIView beginAnimations...] only has an effect when animating view properties--as opposed to layer properties.
Update:
So here is what your code would look like with explicit animation (CATransaction is not required)
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
CATransform3D rotateTransform = CATransform3DMakeRotation(0.3, 0, 0, 1);
CATransform3D scaleTransform = CATransform3DMakeScale(0.10, 0.10, 0.10);
CATransform3D positionTransform = CATransform3DMakeTranslation(24, 423, 0);
CATransform3D combinedTransform = CATransform3DConcat(rotateTransform, scaleTransform);
combinedTransform = CATransform3DConcat(combinedTransform, positionTransform);
[anim setFromValue:[NSValue valueWithCATransform3D:CATransform3DIdentity]];
[anim setToValue:[NSValue valueWithCATransform3D:combinedTransform]];
[anim setDuration:1.0f];
[layerToAnimate addAnimation:anim forKey:nil];
Keep in mind that this only performs the animation. You actually have to set the transform property of the layer with a call to:
[layerToAnimate setTransform:combinedTransform];
as well. Otherwise it will just snap back to its starting position.
Layer properties are animated implicitly whenever you set them except in the case where you are animating the root layer of a view. In that case animations are turned off by default and I've found that what I always have to do instead is animate the view rather than the layer when I am interested in animating the root. So what this means is that a call to any layer within your layer tree that makes a property change will be animated implicitly. For example:
CALayer *root = [view layer];
CALayer *sublayer = [[root sublayers] objectAtIndex:0];
[sublayer setTransform:combinedTransform];
This is the only way I know (knew) you can actually use implicit animation on a layer. However, what your code has pointed out is that you can turn the layer animation on for the root layer simply by placing the changes to the layer within a UIView animation block. That's pretty interesting and handy. This is quite a helpful discovery.
Maybe this is buried in the docs somewhere, but I have yet to come across it.
Setting your layerToAnimate delegate to your rootView.layer works:
viewToAnimate.layer.delegate = rootView.layer;
Assuming you add the view/layer to the root view/layer as well. Cannot find where this changed, implicit animations use to just work.
It turns out you can turn the layer animation on for the root layer by setting its delegate property to nil. I don't know if it's good practice.

Drawing and Animating with UIView or CALayer?

i am trying to implement a graph which a uiview draws. There are 3 UIViews to animate a left/right slide. The problem is, that i can't cancel the UIView animation. So I replaced the UIViews by CALayer. Now, the question is if CALayer is appicable for this? Is it normal to draw on a CALayer like this? And why is a CALayer so slow when I manipulate the frame properties.
CGRect frame = curve.frame;
frame.origin.x -= diffX;
[curve setFrame:frame];
Is there a alternativ?
P.S. I am a german guy. Sorry for mistakes.
I got the animation with CATransaction, but now I will animate a x move with CABasicAnimation. That's no problem expect that the position of the layer go back to the previous x.
CABasicAnimation *theAnimation;
theAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
theAnimation.delegate = self;
theAnimation.duration = 1.0;
theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
CGPoint position = [[curves objectAtIndex:i]position];
position.x = [[curves objectAtIndex:i]position].x - diffX;
[theAnimation setToValue:[NSValue valueWithCGPoint:position]];
[[curves objectAtIndex:i] addAnimation:theAnimation forKey:[NSString stringWithFormat: #"translate.x.%d", index]];
The position changes the position (e.g. xStart = 100, xEnd = 200), but when the animation ends the layer goes back to the beginning x (e.g. x = 100).
Is that normal? How can I solve this problem, that the end position doesn't change anymore?
I tried to changed the removeOnComplete property but that didn't effect.
Hope for help.
Markus
Not sure what you mean by 'slow', but setting the frame of a CALayer in this way uses 'implicit animation'. That is, it will animated the transition from the old frame to the new frame. You can turn this off:
[CATransaction begin];
[CATransaction setValue: (id) kCFBooleanTrue forKey: kCATransactionDisableActions];
[curve setFrame:frame];
[CATransaction commit];
However, this is usually considered an advantage of CALayer. You way want to just use UIViews here, which will not, by default, animate transitions such as this.
Instead of setting the destination position in theAnimation, just set the position property of the thing you want to move.
When you addAnimation:theAnimation, you're setting the "visual style" of any changes to the keyPath property you specified.
When you change the position of the object that the animation is attached to, say from (0,0) to (500,500), CoreAnimation will animate the change for you. The theAnimation object doesn't need the start and end positions, since the underlying object has them.