swiftuicollectionviewuikitcore-animationcakeyframeanimation

swift animateKeyframes withRelativeStartTime not working


I want to achieve an animate: Each cell rightImage move first, and after rightImage moved, the backgroundImage(which is a gif) start to animating. However, the moving animation and gif startAnimating always start at the same time. Like the demo below: enter image description here

Here is Debug Log which print moving timestamp and animating timestamp:

<<< start index:  0
>>> start transfrom 2023-04-04 10:32:39
>>> start animating 2023-04-04 10:32:39
<<< start index:  1
>>> start transfrom 2023-04-04 10:32:43
>>> start animating 2023-04-04 10:32:43
<<< start index:  2
>>> start transfrom 2023-04-04 10:32:47
>>> start animating 2023-04-04 10:32:47

Here is my animation code:

func startMoving() {
        let duration: Double = 4
        UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.calculationModeLinear], animations: {
            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.7/duration, animations: {
                print(">>> start transfrom")
                self.frontImageView.transform = CGAffineTransform(translationX: -20, y: 0)
            })
            UIView.addKeyframe(withRelativeStartTime: 0.7/duration, relativeDuration: 1.5/duration, animations: {
                print(">>> start animating")
                self.backgroundImageView.startAnimating()
            })
        }, completion: { _ in
            self.backgroundImageView.stopAnimating()
        })
    }

Solution

  • Couple of things...

    1 - you are using relative start time and relative duration incorrectly

    2 - keyframe animation don't work the way you think they do

    The relative times are a percentage of the duration.

    If you want the first keyframe to start immediately, and run for 70% of the total duration, it should look like this:

    UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.7, animations: {
    

    and you want the second keyframe to start when the first animation ends (so, 70% from the start), and run for the remainder:

    UIView.addKeyframe(withRelativeStartTime: 0.7, relativeDuration: 0.3, animations: {
    

    So, let's changing the timing parameters, and add a couple more print() statements:

    func startMoving() {
        let duration: Double = 4
        
        UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.calculationModeLinear], animations: {
            
            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.7, animations: {
                print(">>> start transfrom")
                self.frontImageView.transform = CGAffineTransform(translationX: -20, y: 0)
            })
            UIView.addKeyframe(withRelativeStartTime: 0.7, relativeDuration: 0.3, animations: {
                print(">>> start animating")
                self.backgroundImageView.startAnimating()
            })
            
        }, completion: { _ in
            print(">>> completion")
            self.backgroundImageView.stopAnimating()
        })
        
        print(">>> end of startMoving()")
    }
    

    What you'll see in the debug console is immediately:

    >>> start transfrom
    >>> start animating
    >>> end of startMoving()
    

    and at the end of 4-seconds:

    >>> completion
    

    However, you'll still see the animation begin at Zero.

    Keyframe animation does not wait to execute code. Inside the UIView.animateKeyframes block, UIKit analyzes all of the addKeyFrame segments immediately, and then "plays the animation."

    That's why the resulting animation will look different depending on the KeyFrameAnimationOptions (.calulationMode... values).

    Logically, we think that this line: self.backgroundImageView.startAnimating() will execute at 70% of the total duration. In reality, it executes when UIKit builds the animation, which is why your image view animation begins at the start.

    If you want the "front" view to slide over, and then start the image view animation, you need to play the slide-over animation and start the image view animation when that has completed.

    Something along these lines:

    func startMoving() {
        let duration: Double = 4.0 * 0.7
        
        UIView.animate(withDuration: duration, animations: {
            print(">>> start transfrom")
            self.frontImageView.transform = CGAffineTransform(translationX: -20, y: 0)
        }, completion: { _ in
            print(">>> start animating")
            // maybe set imageView animation properties...
            self.backgroundImageView.animationRepeatCount = 1
            self.backgroundImageView.animationDuration = 1.0    // 1-second to cycle through all images
            self.backgroundImageView.startAnimating()
        })
        
        print(">>> end of startMoving()")
    }