1

I want to animate 3 different images at specific point in time such that it behaves this way.

1) 1st image moves from (Xx, Yx) to (Xz,Yz) 
2) Wait 10 seconds
3) 2nd image appears in place at Xa,Yb
4) Wait half as long as in step 2
5) Fade out 2nd image
6) 3rd image appears at the same place as 2nd image

If each of these image's animations are on their own CALayers, can I use CAKeyframeAnimation with multiple layers? If not, what's another way to go about doing staggered animations?

I'm trying to animate a playing card move from offscreen to a particular spot and then few other tricks to appear on screen several seconds later.

Willam Hill
  • 1,572
  • 1
  • 17
  • 28

2 Answers2

3

Edited

When I wrote this, I thought you could not use a CAAnimationGroup to animate multiple layers. Matt just posted an answer demonstrating that you can do that. I hereby eat my words.

I've taking the code in Matt's answer and adapted it to a project which I've uploaded to Github (link.)

The effect Matt's animation creates is of a pair of feet walking up the screen. I found some open source feet and installed them in the project, and made some changes, but the basic approach is Matt's. Props to him.

Here is what the effect looks like:

enter image description here

(The statement below is incorrect) No, you can't use a keyframe animation to animate multiple layers. A given CAAnimation can only act on a single layer. This includes group layers, by the way.

If all you're doing is things like moving images on a straight line, fading out, and fading in, why don't you use UIView animation? Take a look at the methods who's names start with animateWithDuration:animations: Those will let you create multiple animations at the same time, and the completion block can then trigger additional animations.

If you need to use layer animation for some reason, you can use the beginTime property (which CAAnimation objects have because they conform to the CAMediaTiming protocol.) For CAAnimations that are not part of an animation group, you use

animation.beginTime = CACurrentMediaTime() + delay;

Where delay is a double which expresses the delay in seconds.

If the delay is 0, the animation would begin.

A third option would be to set your view controller up as the delegate of the animation and use the animationDidStop:finished: method to chain your animations. This ends up being the messiest approach to implement, in my opinion.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • I wanted to stop the animation when rotation happened which is why I was not looking at UIView animation methods. – Willam Hill Oct 19 '13 at 23:29
  • Can I then use a CAAnimationGroup with multiple layers, with an animationGroup per each layer? – Willam Hill Oct 19 '13 at 23:53
  • A single CAAnimation object can only animate one layer. That includes any CAAnimationGroups that it contains. You can submit multiple animations that run at the same time that animate different layers. – Duncan C Sep 24 '15 at 11:50
  • "A given CAAnimation can only act on a single layer. This includes group layers, by the way." That's false. A single CAAnimationGroup, through its child animations, _can_ refer to, and can animate, multiple sublayers (at any depth) of the layer to which it was added. I'm not saying that the alternatives proposed are not good techniques, but they are not forced upon us as the answer seems to imply. Might be better just to say that chained or delayed animations are "easier" or something. — (Also, keyframe _view_ animations now exist and can obviously animate multiple views.) – matt Sep 02 '20 at 13:56
  • @Matt, this is a very old post, but in my experience a CAAnimationGroup would only manage animations of a single layer. I'd love to be proved wrong. Do you have any samples where you use an animation group to manage animations on multiple sub-layers? (I gather you attach the animation group to the parent layer that contains all the layers you are animating?) – Duncan C Sep 02 '20 at 15:48
  • Yeah, sorry to go on a rampage so long after the fact, but I keep encountering this old claim that an animation group can't manage the animations of multiple layers, and that's not true and never was. The technique is that you add the group to the superlayer, and the key paths of the grouped animations can refer to sublayers as `"sublayers.[name].[property]"`, e.g. `"sublayers.howdy.position"`, where the name (e.g. "howdy") is the sublayer's `name` property value. — And of course, as I say, now that view keyframe animations exist, that would have been a solution too if these are views. – matt Sep 02 '20 at 16:06
  • @matt, the keypath to the animation group (targeted to the parent layer) actually contains the string "sublayers", followed by the named sublayers? How were you able to figure that out, and do you have any sample projects that illustrate it? I went around and around trying to get animation groups to manage animations on multiple layers, and ulitimately gave it up as impossible. (And as you say, there are lots of posts that assert the same thing.) – Duncan C Sep 02 '20 at 16:56
  • I'll give an example as an answer. – matt Sep 02 '20 at 17:03
  • Okay. It's a bit of an archaeological exercise given how old this thread is, but if there is a way to animate multiple layers using animation groups, it would be good to correct the record. I'll certainly update my answer to say "I **thought** you couldn't..." and point it to your answer instead. – Duncan C Sep 02 '20 at 17:09
  • I suppose it _might_ come up in future, though my impression is that Core Animation is gradually being lost to the popular consciousness. Nonetheless, as you rightly say, this claim is quite commonly written into the record and, like many other common claims about Core Animation that I've had to combat over the years, it's wrong. – matt Sep 02 '20 at 17:11
  • Core Animation is quirky and not very well documented, but it's quite powerful. There are things you just can't do with the much-simpler-to-use UIView animations. Mask animations are one example. – Duncan C Sep 03 '20 at 01:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/220887/discussion-between-duncan-c-and-matt). – Duncan C Sep 03 '20 at 01:43
3

The claim that a single animation group cannot animate properties of different layers is not true. It can. The technique is to attach the animation group to the superlayer and refer to the properties of the sublayers in the individual animations' key paths.

Here is a complete example just for demonstration purposes. When launched, this project displays two "footprints" that proceed to step in alternation, walking off the top of the screen.

class ViewController: UIViewController, CAAnimationDelegate {

    let leftfoot = CALayer()
    let rightfoot = CALayer()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.leftfoot.name = "left"
        self.leftfoot.contents = UIImage(named:"leftfoot")!.cgImage
        self.leftfoot.frame = CGRect(x: 100, y: 300, width: 50, height: 80)
        self.view.layer.addSublayer(self.leftfoot)

        self.rightfoot.name = "right"
        self.rightfoot.contents = UIImage(named:"rightfoot")!.cgImage
        self.rightfoot.frame = CGRect(x: 170, y: 300, width: 50, height: 80)
        self.view.layer.addSublayer(self.rightfoot)

        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.start()
        }
    }

    func start() {
        let firstLeftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
        firstLeftStep.byValue = -80
        firstLeftStep.duration = 1
        firstLeftStep.fillMode = .forwards

        func rightStepAfter(_ t: Double) -> CABasicAnimation {
            let rightStep = CABasicAnimation(keyPath: "sublayers.right.position.y")
            rightStep.byValue = -160
            rightStep.beginTime = t
            rightStep.duration = 2
            rightStep.fillMode = .forwards
            return rightStep
        }
        func leftStepAfter(_ t: Double) -> CABasicAnimation {
            let leftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
            leftStep.byValue = -160
            leftStep.beginTime = t
            leftStep.duration = 2
            leftStep.fillMode = .forwards
            return leftStep
        }

        let group = CAAnimationGroup()
        group.duration = 11
        group.animations = [firstLeftStep]
        for i in stride(from: 1, through: 9, by: 4) {
            group.animations?.append(rightStepAfter(Double(i)))
            group.animations?.append(leftStepAfter(Double(i+2)))
        }
        group.delegate = self
        self.view.layer.add(group, forKey: nil)
    }
    
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        print("done")
        self.rightfoot.removeFromSuperlayer()
        self.leftfoot.removeFromSuperlayer()
    }

}

Having said all that, I should add that if you are animating a core property like the position of something, it might be simpler to make it a view and use a UIView keyframe animation to coordinate animations on different views. Still, the point is that to say that this cannot be done with CAAnimationGroup is just wrong.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Very cool. (upvoted.) I'll have to play with it. Do you have a sample project on a repo like github somewhere that you can share? – Duncan C Sep 02 '20 at 17:13
  • Just use any two images and copy and paste the code I gave. Works right out of the box. – matt Sep 02 '20 at 17:15
  • 1
    I took the liberty of creating a Github project based on your code. It works beautifully. You can download it at the following link: https://github.com/DuncanMC/AnimationGroups/tree/master/AnimationGroups – Duncan C Sep 03 '20 at 00:03
  • 1
    I modified the code to calculate layer positions and animation step sizes based on imageHeight and imageWidth variables, and then calculate the number of steps it can take based on current screen height. – Duncan C Sep 03 '20 at 00:04
  • 1
    Note that I also attach a completion handler closure to my animations. I then set up my view controller to look for that closure, and if it finds it, run it. That makes it possible to have very flexible animation completion methods. – Duncan C Sep 03 '20 at 01:22
  • It will not surprise you that I think _that_ is the coolest part. Nice idea. – matt Sep 03 '20 at 01:24
  • The fact that CAAnimation allows you to attach arbitrary objects using `setValue(_:, forKey:)` is quite powerful. Without that you'd have to resort to Objective-C associated values. That's another quite-powerful trick that neverthless makes me cringe. – Duncan C Sep 03 '20 at 01:29
  • How did you figure out the `keyPath: "sublayers."` trick? I flogged at using CALayerGroups to animated multiple layers for a LONNNNNG time, and eventually gave up. – Duncan C Sep 03 '20 at 01:37
  • Dang I am pretty versed in Core Animation and I don’t think that I knew or ever used sublayers name path. That is super cool. So sublayers.name.property will become a new weapon for me. Thanks Matt. – agibson007 Sep 14 '20 at 11:54