Sublayer not permanently updating after CABasicAnimation is added [duplicate] - swift

I am using CABasicAnimation to move and resize an image view. I want the image view to be added to the superview, animate, and then be removed from the superview.
In order to achieve that I am listening for delegate call of my CAAnimationGroup, and as soon as it gets called I remove the image view from the superview.
The problem is that sometimes the image blinks in the initial location before being removed from the superview. What's the best way to avoid this behavior?
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
animGroup.duration = .5;
animGroup.delegate = self;
[imageView.layer addAnimation:animGroup forKey:nil];

When you add an animation to a layer, the animation does not change the layer's properties. Instead, the system creates a copy of the layer. The original layer is called the model layer, and the duplicate is called the presentation layer. The presentation layer's properties change as the animation progresses, but the model layer's properties stay unchanged.
When you remove the animation, the system destroys the presentation layer, leaving only the model layer, and the model layer's properties then control how the layer is drawn. So if the model layer's properties don't match the final animated values of the presentation layer's properties, the layer will instantly reset to its appearance before the animation.
To fix this, you need to set the model layer's properties to the final values of the animation, and then add the animation to the layer. You want to do it in this order because changing a layer property can add an implicit animation for the property, which would conflict with the animation you want to explicitly add. You want to make sure your explicit animation overrides the implicit animation.
So how do you do all this? The basic recipe looks like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:myLayer.position];
layer.position = newPosition; // HERE I UPDATE THE MODEL LAYER'S PROPERTY
animation.toValue = [NSValue valueWithCGPoint:myLayer.position];
animation.duration = .5;
[myLayer addAnimation:animation forKey:animation.keyPath];
I haven't used an animation group so I don't know exactly what you might need to change. I just add each animation separately to the layer.
I also find it easier to use the +[CATransaction setCompletionBlock:] method to set a completion handler for one or several animations, instead of trying to use an animation's delegate. You set the transaction's completion block, then add the animations:
[CATransaction begin]; {
[CATransaction setCompletionBlock:^{
[self.imageView removeFromSuperview];
}];
[self addPositionAnimation];
[self addScaleAnimation];
[self addOpacityAnimation];
} [CATransaction commit];

CAAnimations are removed automatically when complete. There is a property removedOnCompletion that controls this. You should set that to NO.
Additionally, there is something known as the fillMode which controls the animation's behavior before and after its duration. This is a property declared on CAMediaTiming (which CAAnimation conforms to). You should set this to kCAFillModeForwards.
With both of these changes the animation should persist after it's complete. However, I don't know if you need to change these on the group, or on the individual animations within the group, or both.

Heres an example in Swift that may help someone
It's an animation on a gradient layer. It's animating the .locations property.
The critical point as #robMayoff answer explains fully is that:
Surprisingly, when you do a layer animation, you actually set the final value, first, before you start the animation!
The following is a good example because the animation repeats endlessly.
When the animation repeats endlessly, you will see occasionally a "flash" between animations, if you make the classic mistake of "forgetting to set the value before you animate it!"
var previousLocations: [NSNumber] = []
...
func flexTheColors() { // "flex" the color bands randomly
let oldValues = previousTargetLocations
let newValues = randomLocations()
previousTargetLocations = newValues
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
// and by the way, this is how you endlessly animate:
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.animeFlexColorsEndless()
}
let a = CABasicAnimation(keyPath: "locations")
a.isCumulative = false
a.autoreverses = false
a.isRemovedOnCompletion = true
a.repeatCount = 0
a.fromValue = oldValues
a.toValue = newValues
a.duration = (2.0...4.0).random()
theLayer.add(a, forKey: nil)
CATransaction.commit()
}
The following may help clarify something for new programmers. Note that in my code I do this:
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
...set up the animation...
CATransaction.commit()
however in the code example in the other answer, it's like this:
CATransaction.begin()
...set up the animation...
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
CATransaction.commit()
Regarding the position of the line of code where you "set the values, before animating!" ..
It's actually perfectly OK to have that line actually "inside" the begin-commit lines of code. So long as you do it before the .commit().
I only mention this as it may confuse new animators.

Related

Swift Make Effect of Animation Permanent

I have looked all around but I haven't been able to find an answer to this simple question. Is there a way to make the effect of a Swift CABasicAnimation animation permanent? (Meaning that when the animation is over, the view won't reset to the state it was in before the animation began.)
Is there a way to make the effect of a Swift CABasicAnimation animation permanent
Absolutely. Just set the same property that you are animating, to the value that it will have when the animation is over. You can do this at the same time you create and add the animation. (You'll probably want to turn implicit animation off when you do this, so that you don't animate this change as well.)
Example from my own code (I'm animating the transform of a layer called arrow so that it rotates):
let startValue = arrow.transform
let endValue = CATransform3DRotate(
startValue, CGFloat(M_PI)/4.0, 0, 0, 1)
// change the layer, without implicit animation
// THIS IS WHAT YOU ARE ASKING ABOUT
CATransaction.setDisableActions(true)
arrow.transform = endValue
// construct the explicit animation
let anim = CABasicAnimation(keyPath:"transform")
anim.duration = 0.8
anim.fromValue = NSValue(CATransform3D:startValue)
anim.toValue = NSValue(CATransform3D:endValue)
// ask for the explicit animation
arrow.addAnimation(anim, forKey:nil)

Why can't I set the opacity of a layer after a CABasicAnimation finished?

I have a view with a sublayer called specialLayer. Implicit animations for opacity are disabled like this:
self.specialLayer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], #"opacity", nil];
There are only two methods which access this sublayer.
- (void)touchDown {
self.specialLayer.opacity = 0.25f;
}
- (void)touchUp {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = [NSNumber numberWithFloat:self.specialLayer.opacity];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.duration = 0.5;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[self.specialLayer addAnimation:animation forKey:#"opacity"];
}
This happens:
1) User presses a button down (holding) and -touchDown gets called. specialLayer's opacity instantly becomes 0.25
2) User releases button and -touchUp gets called. specialLayer's opacity animates back to 1.0
This sequence only works ONCE!
3) User presses a button down again (holding) and -touchDown gets called. But specialLayer's opacity does not change on screen! Nothing happens even though self.specialLayer.opacity = 0.25f; gets called. Opacity appears to remain 1.0f.
4) User lifts finger up and -touchUp gets called. specialLayer's opacity JUMPS to 0.25 and from there animates to 1.0f.
I checked with NSLog to make sure the methods get called in this order. They do.
When I uncomment the animation code and set the opacity directly to 1.0, subsequent direct opacity changes are effective immediately. It is definitely the CABasicAnimation which causes problems. Removing the code that disables implicit animations has no effect on this problem.
I can't for the life of me figure out what is happening here. Why does CALayer not reflect the opacity value I set to it some time later after I added a CABasicAnimation for opacity? Is this a bug?
What am I doing wrong?
You set the animation's removedOnCompletion to NO. This means that when the animation completes, it still stays on the layer and still controls the value of opacity for the purposes of rendering. When you call this code a second time, the new animation replaces the old, which causes the jump (as it's now using the newly-set 0.25f as the start point).
The solution is to just stop setting removedOnCompletion to NO. Instead, you should just set the actual opacity of the layer to the same value as the endpoint of the animation., at the same time as you add the animation.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
// you can omit fromValue since you're setting it to the layer's opacity
// by omitting fromValue, it uses the current value instead. Just remember
// to not change layer.opacity until after you add the animation to the layer
animation.toValue = [NSNumber numberWithFloat:1];
animation.duration = 0.5;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[layer addAnimation:animation forKey:#"opacity"];
layer.opacity = 1; // or layer.opacity = [animation.toValue floatValue];
The reason for this is because of the way layer rendering works. When CoreAnimation renders a layer tree to the screen, it copies the model tree and produces what's called the presentation tree. It does this by copying all the layers, and then applying all the animations. So it goes over each animation on the layer, plugs the time into the timing function, plugs the resulting progress into the interpolation between the from/to values, and applies that final value back onto the presentation layer's property.
So when you leave an animation on the layer permanently, this animation will always override whatever the actual model value is for the key. It doesn't matter how many times you change layer.opacity, the animation that controls opacity will always overwrite the value for presentation purposes.
This same reason is also why you can set the layer.opacity to whatever you want after adding the animation, because the animation will take precedence. Only when the animation is removed (which happens by default when the animation completes) will the model value for that property get used again.

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.

How to move a view or label in x direction using CABasicAnimation in iPhone

I am using the following code to move a label from one position to another in x direction
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
theAnimation.duration=1;
theAnimation.repeatCount=1;
theAnimation.autoreverses=NO;
theAnimation.fromValue=[NSNumber numberWithFloat:0];
theAnimation.toValue=[NSNumber numberWithFloat:80];
[lbl.layer addAnimation:theAnimation forKey:#"animateLayer"];
But in this case at the end of the animation the label shifts back to its original position. How to make sure it stays at the position where it is moved.
Is there any good way to do it without using timer and changing the coordinates on our own.
After the animation completes, it is removed. That's why it snaps back. Add this to your animation:
theAnimation.removedOnCompletion = NO;
theAnimation.fillMode = kCAFillModeForwards;
This will prevent the animation from being removed, and tells the animation to remain in its final state upon completion.
There are 2 items that need updating here. The presentation layer and the model. CABasicAnimation changes only the presentation layer and never updates the model. So when the presentation layer from the animation finishes, it goes away and you see the view with the values from the model. You just have to update the model with the new value when the animation is done.
[layer setValue:toValue forKeyPath:keyPath];
have a look at a utility i wrote to help out with exactly with this, HMBasicAnimation
http://hellomihai.wordpress.com/2014/09/02/hmbasicanimation-utility/
usage:
[HMBasicAnimation doAnimation:myView.layer // layer youre updating
toValue:myView.frame.size.width/2 // your value
duration:1.5 // duration
delaySeconds:1 // animation delay (good for chaining animations
keyPath:HMBasicAnimation_TRANSLATION_X]; // what you're changing, several available

combining animations on single property

How can I use two separate CABasicAnimations to simultaneously animate the same property?
For example Two CABasicAnimations, both animate position.y.
The first animation will be a bounce (from 100, to 300, duration = 1, autoreverse = yes, repeatcount = 10)
The second animation will be a slow scroll (by 100, duration = 10)
The behavior I am seeing is that if the first animation is in progress & I use:
[pickle.layer addAnimation:[self makescrollanimation] forKey:#"scrollit"];
to add the second... the second is ignored.
I know the second animation works, because if i start with the second one, then the first is ignored.
Thank you-
Matt
If I understand you correctly, you want the view to bounce up and down while the whole bouncing motion shifts slowly downwards. You can do this by making the animations additive using their additive property. For example:
CABasicAnimation *bounceAnimation = [CABasicAnimation animationWithKeyPath:#"position.y"];
bounceAnimation.duration = 1;
bounceAnimation.fromValue = [NSNumber numberWithInt:100];
bounceAnimation.toValue = [NSNumber numberWithInt:300];
bounceAnimation.repeatCount = 10;
bounceAnimation.autoreverses = YES;
bounceAnimation.fillMode = kCAFillModeForwards;
bounceAnimation.removedOnCompletion = NO;
bounceAnimation.additive = YES;
[view.layer addAnimation:bounceAnimation forKey:#"bounceAnimation"];
CABasicAnimation *scrollAnimation = [CABasicAnimation animationWithKeyPath:#"position.y"];
scrollAnimation.duration = 10;
scrollAnimation.fromValue = [NSNumber numberWithInt:0];
scrollAnimation.toValue = [NSNumber numberWithInt:100];
scrollAnimation.fillMode = kCAFillModeForwards;
scrollAnimation.removedOnCompletion = NO;
scrollAnimation.additive = YES;
[view.layer addAnimation:scrollAnimation forKey:#"scrollAnimation"];
should accomplish the animation you desire, if I'm reading your question correctly. The scroll animation should be able to be triggered at any point during the bounce animation.
I wrote up a quick summary about how to use additive animation
In short, in a transaction set the new model value, then you animate from the old model value minus the new model value, and the destination is NSZeroPoint, NSZeroRect, or the identity transform. You get beautiful curves instead of a jolt from changing an animation in-flight.
You can use CAAnimationGroup if you want to bundle multiple animations across different properties, but I'm not sure this is possible using two CABasicAnimations. You can only apply one property animation per property to a view at a time.
One way I could think of accomplishing this would be to nest your view in another UIView, and perform the 'scrollit' animation on the enclosing view, while continuing to bounce the enclosed view.
Love to know a better answer!