Thanks to @matt, I learned a while back that it is possible to animate multiple layers' properties in a CAAnimationGroup
. The trick is to add name the child layer(s).
See Matt's answer in this thread for a discussion of the technique, plus a working example I wrote to test it out.
You then add the animation group to the parent layer, and in the animations that affect the sublayers of the parent layer, you create those sublayer animations using a keyPath
value that references the child layer as a named sublayer of the parent layer.
So if I have CAShapeLayer parentLayer
, that also has a sublayer named "sublayer",
let parentLayer = CAShapeLayer()
let sublayer = CAShapeLayer()
sublayer.name = "sublayer"
parentLayer.addSublayer(sublayer)
And I want to create an animation group that animates properties of the parent layer and the sublayer at the same time, that code might look like this:
let animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
let parentLayerAnimation = CABasicAnimation(keyPath: "path")
parentLayerAnimation.duration = animationDuration
parentLayerAnimation.fromValue = parentLayer.path
parentLayerAnimation.toValue = newPath
let sublayerAnimation = CABasicAnimation(keyPath: "sublayers.sublayer.path")
sublayerAnimation.duration = animationDuration
sublayerAnimation.fromValue = subLayer.path
sublayerAnimation.toValue = newSublayerPath
let animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
animationGroup.animations = [parentLayerAnimation, sublayerAnimation]
parentLayer.add(animationGroup, forKey: nil)
The code above does not work. The problem appears to be specifically with trying to animate the path of the sublayer. If I instead animate the sublayer's rotation, using a code like this:
let sublayerRotationAnimation = CABasicAnimation(keyPath: "sublayers.sublayer.transform.rotation.z")
sublayerRotationAnimation.duration = animationDuration
sublayerRotationAnimation.fromValue = 0
sublayerRotationAnimation.toValue = CGFloat.pi
And add the sublayerRotationAnimation
to the animation group, that works.
The problem seems to occur if I try to animate the sublayer's path. Animating other properties works.
(And if I change the key path of the sublayer's animation to just "path" and add the animation directly to the sublayer, that works too.)
The effect I am after is this:
(The black caret that animates into an arc is the parent layer, and the red lines with the dots are the sublayer bezierIllustrationLayer
.)
I have a branch of a working project on Github that illustrates this problem:
Github project branch with animation group that doesn't work
The main branch of the project simply submits separate animations to the layer and the sublayer, and that works.
The code in that branch that attempts to add an animation group is in the file CaretToArcView.swift:
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = animationDuration
pathAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
pathAnimation.fromValue = layer.path
pathAnimation.toValue = path.cgPath
// This animation does not work when added to an animation group applied to the parent layer of the bezierIllustrationLayer
let bezierPathAnimation = CABasicAnimation(keyPath: "sublayers.bezierillustrationlayer.path")
bezierPathAnimation.duration = animationDuration
bezierPathAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
bezierPathAnimation.fromValue = bezierIllustrationLayer.path
bezierPathAnimation.toValue = illustrationPath.cgPath
//If I add this rotation animation to the animation group, it works
//let rotationAnimation = CABasicAnimation(keyPath: "sublayers.bezierillustrationlayer.transform.rotation.z")
//rotationAnimation.duration = 1.0
//rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
//rotationAnimation.fromValue = 0
//rotationAnimation.toValue = CGFloat.pi * 2
let animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animationGroup.animations = [pathAnimation, bezierPathAnimation]
layer.add(animationGroup, forKey: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
layer.path = path.cgPath
self.bezierIllustrationLayer.path = illustrationPath.cgPath
}
As far as I can tell, this is a bug in CAAnimationGroup. The same animation works if I apply it directly to the sublayer (after changing the keyPath appropriately). An animation of a different property like the sublayer's transform.rotation.z
works in an animation group. It's just animating the sublayer's path that fails.
Has anybody else encountered this, and do you have a solution? Matt? I think you're the reigning expert on CAAnimation here. Can you help?