Continuous CABasicAnimation independent of parent layer animations - ios

I am unable to figure out if it is possible to execute a CABasicAnimation independently of possible super view animations.
Let's say I want to have a continuous spinning sub view:
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotation setFromValue:#(0.0)];
[rotation setToValue:#(2.0 * M_PI)];
[rotation setDuration:2.0];
[rotation setRepeatCount:HUGE_VALF];
[[_rotatingView layer] addAnimation:rotation forKey:#"Rotate"];
This works. The problem is that in some super view upwards the view chain the speed and time offset of the layer of that view is used to manually control some animations.
This automatically controls this rotation animation. I would like this rotation animation (at least the timing part of it) to run independently and not be influenced by possible animations up the view chain.
Is this possible?

Related

CABasicAnimation does not animate correctly when I update model layer

I'm currently implementing a CABasicAnimation that animates a CALayer transform property.
Now, although I'm new to Core Animation, I have been able to gather through various blogs and articles such as objc.io that it is a very bad idea to use the often (incorrectly) recommended method for getting animations to stick using the fillMode and removedOnCompletion properties of an animation. This method is considered bad practice by many because it creates a discrepancy between the model layer and the presentation layer, and so future queries to one of those layers may not match up with what the user is seeing.
Instead, the recommended way of doing animations is to update the model layer at the same time you add the animation to the layer being animated. However, I'm having trouble understanding exactly how this is working. My animation is simple, and goes like this:
CATransform3D updatedTransform = [self newTransformWithCurrentTransform];
// Location 1
CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
transformAnimation.duration = 1;
transformAnimation.fromValue = [NSValue valueWithCATransform3D:self.layerBeingAnimated.transform]; // Does not work without this.
transformAnimation.toValue = [NSValue valueWithCATransform3D:updatedTransform];
// Location 2
[self.layerBeingAnimated addAnimation:transformAnimation forKey:kTransformAnimationKey];
// Location 3
I've denoted three locations where I have attempted to update the model layer using the code
self.layerBeingAnimated.transform = updatedTransform;
In location 1, the layer jumps right to newTransform and does not animate.
In location 2, the layer animates exactly as I want it to from the current transform to newTransform.
In location 3, the layer jumps right to newTransform, jumps back to the the old transform, animates correctly from the fromValue to newTransform, and then stays at newTransform.
What's the deal here? What's the correct location to update the model layer and why are these three locations producing such different results?
Thanks!
I think that it's easiest to explain what is happening for each of the three locations and then a "conclusion" at the end.
I'm also adding some illustrations, showing exactly the behaviour that you are mentioning in your question so that it will be easier to follow for someone who hasn't tried these three things themselves. I'm also extending the illustration to show both a stand alone layer and a backing layer (one that is attached to a view) and I will explain the difference where there is one.
Location 1
In the first location, the model value is updated before the animation is created. Once this is done, the transform property holds the updatedTransform. This means that when you read the transform from the layer for the fromValue, you get the updatedValue back. This in turn means that both to and from value are the same so you can't see the animation.
One thing that could have made this location work as expected is to read the oldValue before assigning the new value and then use that as the fromValue. This will look as expected.
// Location 1
CATransform3D oldValue = layer.transform; // read the old value first
layer.transform = updatedTransform; // then update to the new value
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.duration = 1.0;
anim.fromValue = [NSValue valueWithCATransform3D:oldValue];
anim.toValue = [NSValue valueWithCATransform3D:updatedTransform];
Location 2
In the second example, the value isn't yet updated when you read the transform for the from value, so the fromValue and toValue are different. After that, the model value is updated to it's final value. There is actually a difference between the stand-alone layer and the backing layer here, but we don't see it. The transform property on CALayer is animatable and will automatically perform an "implicit" animation when the value changes. This means that an animation will be added to the layer for the "transform" key path. The view, however, disables this behaviour when the change happens outside of an animation block, so there is not implicit animation there.
The reason why we don't see the implicit animation is that the "explicit" animation is added afterwards for the same key path. This means that the only explicit animation will be visible, in both cases, even thought there are two animation running on the stand-alone layer (more on that later). If you are feeling cautious, then you could disable the implicit action for the stand-alone layer (more on that later).
Location 3
This leaves us with the last location. In this case the animation is created just as above, with different fromValue and toValue. The only difference is the order of adding the explicit animation and changing the property which triggers an implicit animation. In this case the implicit animation is added after the explicit animation and they both run(!). Both animations actually ran for location 2, but we couldn't see it because the explicit (longer) animation was added before.
Since everything is moving so fast, I slowed down the entire layer to try and illustrate what is happening when two animations are running at once. This way it becomes much easier to see what happens when the implicit animation ends. I've overlaid the well behaving backing layer and the misbehaving stand-alone layer and made them both 50% transparent. The dashed outline is the original frame.
A short description of what is happening: the blue view get's only the explicit animation added to it (which has a 1 second duration). The orange layer first has the same explicit animation added to it and then has an implicit 0.25 second animation added to it. Neither explicit nor the implicit animations are "additive", meaning their toValue and fromValue are used as-is.
Disclaimer: I do not work at Apple and I haven't seen the source code for Core Animation so what I'm about to say is guesswork based on how things behave.
In my understanding (see disclaimer) this is what happens for every screen refresh to produce the animation: for the current time stamp, the layer goes through the animations in the order they were added and updates the presentation values. In this case, the explicit animation sets a rotation transform, then the implicit animation comes and sets another rotation transform that completely overrides the explicit transform.
If an animation is configured to be "additive", it will add to the presentation values instead of overwriting (which is super powerful). Even with additive animations, order still matters. A non-additive animation could come later and overwrite the whole thing.
Since the implicit animation is shorter than the explicit one, we see that for the first part of the total animation, the values are strictly coming from the implicit animation (which was added last). Once the implicit animation finishes, the only remaining animation is the explicit animation which has been running underneath the implicit one, all this time. So when the implicit animation finishes, the explicit animation has already progressed 0.25 seconds and we see that the orange layer jumps back to the same value as the blue view, instead of jumping back to the beginning.
Where should we update the value?
At this point, the question is, how can we prevent two animations from being added and where should we update the value? The location where the value is updated doesn't prevent there from being two animations (but it can affect how the end result looks).
To prevent two actions from being added to the stand-alone layer, we temporarily disable all "actions" (a more general term for an animation):
[CATransaction begin];
[CATransaction setDisableActions:YES]; // actions are disabled for now
layer.transform = updatedTransform;
[CATransaction commit]; // until here
When we do this, only one animation is added to the layer so either location 2 or 3 works. That is simply a matter of taste. If you read the oldValue, the you can also use location 1 (as long as the action is disabled).
If you are animating a backing layer then you don't have to disable actions (the view does that for you) but it also doesn't hurt to do so.
At this point I could keep going about other ways to configure an animation, what an additive animation is, and why you needed to specify both the toValue and fromValue in this case. But I think that I have answered the question you asked and that this answer already is a bit on the long side.

I am animating the path value of a filled but not stroked CAShapeLayer and I would like to make it animate more smoothly

Alright, I'll just say that again:
I am animating the path value of a filled but not stroked CAShapeLayer and I would like to make it animate more smoothly.
Here is the basic animation code:
CABasicAnimation * pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.toValue = (id)targetPath.CGPath;
pathAnimation.fromValue = (id)shapePath.CGPath;
//pathAnimation.fromValue = (id)((CAShapeLayer*)[currentShapeLayers objectForKey:key]).path;
pathAnimation.repeatCount = 0;
pathAnimation.duration = animationTime;
pathAnimation.autoreverses = NO;
[[currentShapeLayers objectForKey:key] addAnimation:pathAnimation forKey:#"animatePath"];
Eh, it works alright. But the paths are oddly shaped and dynamically formed. I tried ensuring that the number of points is always the same between paths, and that kind of helps. But I still get parts of the paths animating that I don't want to animate. Really what I'd like to do is keep part of the path static while the rest of the path grows out of one side or shrinks back into it or, in some cases, separates from the main path into a separate subpath.
I've considered finding intermediary path stages and animating to those first, and then finishing the animation from there, but I feel like there may be another solution.
I found a solution to this. I'm not sure if it's the most efficient solution, since it involves a lot of redrawing, but it does allow a lot of control over how the animation behaves.

Using CALayer's <CAMediaTiming> methods to delay implicit animations

I'm trying to affect the timing of implicit CALayer animations using the CAMediaTiming protocol implemented by CALayer.
So far, it seems I have to do
[CATransaction begin];
[CATransaction setAnimationDuration:d];
layer.frame = newFrame;
[CATransaction commit];
layer.timeOffset = -1;
to create a 1 second delay for the implicit animation
Unfortunately what seems to happen here is very briefly the animation starts (the layer moves a tiny amount) and then pops back into place and waits for 1 second before starting the actual animation.
I believe that this is due to a delay in propagating the timeOffset information to the render tree. I've tried playing around with [CATransaction flush], but haven't found an arrangement that doesn't have the initial stutter.
Has anyone successfully used a CALayer's CAMediaTiming methods to change the timing of implicit animations?
I found http://wangling.me/2011/06/time-warp-in-animation.html quite useful when using CAMediaTiming. Especially the explanations regarding timeOffset and beginTime might be of interest to you.

how can I chain and group Core Animations for different objects?

Say I have 3 CALayers (squares, of different colors, for the sake of example).
And I want to perform the following animation:
layer1 simultaneously translates and scales from position A to position B.
Once layer1 is in its new position and has its new size, layer2 and 3 simultaneously translate from positions C and D to positions E and F, respectively. These layers (2 and 3) also fade in as they translate to E and F. But layer2 also scales as it translates and layer3 just translates (but with a CAKeyframeAnimation so that when it reaches point F it does a little bouncy up-down effect).
For layer 1, I can:
create a CABasicAnimation with the only setting of duration set to,
say 1, and add it to layer1 for key "transform" creating a translate
scale transform and applying it to layer
For layer2 same thing as for layer1 but also setting the opacity.
For layer3 I'd create a keyframe animation. And also set the opacity.
From the above:
Seems like calling layer2 setTransform: followed by setOpacity: results in simultaneous animations. But is this the right way to make sure they are simultaneous or should I use something like CAAnimationGroup?
Does it make sense to translate and scale by concatenating matrices? Does it even make sense to use matrices as opposed to setting positions?
How do I chain these, so that the animations for layer2 and layer3 won't start until layer1 is done and then, layer2's transform and opacity happen at the same time as layer3's keyframe and opacity animations?
I did something extremely similar today.. I was mixing CGAffineTransform and CABasicAnimations and it caused the most awkward error.. anyways.
to set animation to perform a method after completion I'm awful sure I was using:
[self performSelector:#selector(animationDone) withObject:nil afterDelay:0.0];
after I finished making 3 CABasicAnimations in one method I used, CAAnimationGroup to call all 3 at once. After the completion of the 1st animation group it would call the animationDone method to start next method of subsequent animations.
also had to be careful of the:
animationObject.fillMode = kCAFillModeForwards; //b/c of object's coordinate changes
animationObject.removedOnCompletion = NO;

CAKeyframeAnimation and sprite sheet animation problem

I have a CAKeyframeAnimation of a sprite sheet's contentsRects property to show an animation in a CALayer. Everything works most of the time, but occasionally I see a wrong part of sprite sheet for a one frame. CAKeyframeAnimation is set to discrete mode and all the coordinates are checked. Seems to me that once in a while CAKeyframeAnimation misses a keyframe and shows me just a center piece of my sprite sheet instead of a piece that it suppose to cut out based on provided array of contensRects. How to fix the above? Also on the same topic how to debug CAKeyframeAnimation in general and find out what's going on?
thanks!
to fix it I've wrapped my animation in CATransaction and temporarily disabled all layer actions:
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; // we need to disable all layer actions temporarily
.............. the rest of animation code ...............
[CATransaction commit];
thank you Apple's documentation!

Resources