Search code examples
iosanimationcore-animationcalayercabasicanimation

Start CABasicAnimation with an offset


I'm having real trouble understanding the timing system of CABasicAnimation, especially the beginTime and timeOffset properties. I went through this tutorial and questions such as this one, which both explain a way of pausing/resuming animation. Still, I can't figure out how to start an animation directly with an offset.

I would like, for instance, to start my animation at 2.0s, meaning that if my animation is a color transition from white to black with a duration of 3.0s, then the animation would start from dark gray and transition to black in 1.0s.

I can't use UIViewPropertyAnimator because my animation is about changing the colors of a gradient (CAGradientLayer).

How can I start my animation with the offset I want, when I add it to a layer?


Solution

  • To start a CABasicAnimation directly with an offset, it's as simple as setting its timeOffset to the time within the animation's timeline you want it to start.

    So for example, if your animation's duration is set to 3, and the timeOffset is set to 2, then the animation will (visibly) begin at 2 seconds into the animation.

    However, this does not stop the animation from then wrapping back around to finish the part of the animation that was skipped (from 0-2 seconds). If you want the animation to stop after it gets to its toValue, you can set repeatDuration to 1 to cut off the animation after 1 second.

    Here's a complete Playground example where you can play around with the animation properties and click on the live view to see the animation:

    import UIKit
    import PlaygroundSupport
    
    class GradientView: UIView {
        override class var layerClass: AnyClass {
            CAGradientLayer.self
        }
    
        override var layer: CAGradientLayer {
            super.layer as! CAGradientLayer
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            layer.colors = [UIColor.white.cgColor, UIColor.white.cgColor]
            layer.locations = [0, 1]
    
            let tap = UITapGestureRecognizer(target: self,
                                             action: #selector(animateGradient))
            addGestureRecognizer(tap)
        }
    
        required init?(coder: NSCoder) {
            fatalError("Not implemented")
        }
    
        @objc func animateGradient() {
    
            // Animate the `colors` property of the gradient layer.
            // In this example we're just animating the first color of
            // a two-color gradient.
            let anim = CABasicAnimation(keyPath: "colors")
            anim.fromValue = [UIColor.white.cgColor, UIColor.white.cgColor]
            anim.toValue = [UIColor.black.cgColor, UIColor.white.cgColor]
    
            // Sets the length of the animation's timeline.
            //
            // Note that in this case, this is not the _effective_ duration
            // as we are stopping the animation after 1 second due to setting
            // `repeatDuration` below.
            anim.duration = 3
    
            // Where in the animation's timeline the animation should
            // effectively start.
            //
            // In this case, this starts the animation 2 seconds in to the
            // timeline, which makes it look like the first gradient color
            // immediately starts at dark gray.
            anim.timeOffset = 2
    
            // Stops the animation when it gets to the `toValue`.
            // This makes the effective duration 1 second long.
            //
            // Comment this out to let the animation loop back around
            // so that it then fades from white to dark gray, if desired.
            anim.repeatDuration = anim.duration - anim.timeOffset
    
            layer.add(anim, forKey: nil)
        }
    }
    
    let gradientViewFrame = CGRect(x: 0, y: 0, width: 200, height: 200)
    PlaygroundPage.current.liveView = GradientView(frame: gradientViewFrame)