Search code examples
iosswiftcaanimation

Modify a value during a CAAnimation


Say you have a simple animation

    let e : CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
    e.duration = 2.0
    e.fromValue = 0 
    e.toValue = 1.0
    e.repeatCount = .greatestFiniteMagnitude
    self.someLayer.add(e, forKey: nil)

of a layer

private lazy var someLayer: CAShapeLayer

It's quite easy to read the value of strokeEnd each animation step.

Just change to a custom layer

private lazy var someLayer: CrazyLayer

and then

class CrazyLayer: CAShapeLayer {
    override func draw(in ctx: CGContext) {
        print("Yo \(presentation()!.strokeEnd)")
        super.draw(in: ctx)
    }
}

It would be very convenient to actually be able to set the property values of the layer at each step.

Example:

class CrazyLayer: CAShapeLayer {
    override func draw(in ctx: CGContext) {
        print("Yo \(presentation()!.strokeEnd)")
        strokeStart = presentation()!.strokeEnd - 0.3
        super.draw(in: ctx)
    }
}

Of course you can't do that -

attempting to modify read-only layer

Perhaps you could have a custom animatable property

class LoopyLayer: CAShapeLayer {
    @NSManaged var trick: CGFloat

    override class func needsDisplay(forKey key: String) -> Bool {
        if key == "trick" { return true }
        return super.needsDisplay(forKey: key)
    }

    ... somehow for each frame
    strokeEnd = trick * something
    backgroundColor = alpha: trick
}

How can I "manually" set the values of the animatable properties, each frame?

Is there perhaps a way to simply supply (override?) a function which calculates the value each frame? How to set values each frame, on some of the properties, from a calculation of other properties or perhaps another custom property?

(Footnote 1 - a hacky workaround is to abuse a keyframe animation, but that's not really the point here.)

(Footnote 2 - of course, it's often better to just simply animate in the ordinary old manual way with CADisplayLink, and not bother about CAAnimation, but this QA is about CAAnimation.)


Solution

  • Roughly speaking, CoreAnimation gives you the ability to perform the interpolation between certain values according to a certain timing function.

    CAAnimation has a private method that applies its interpolated value to the layer at a given time.

    The injection of that value into the presentation layer happens after the presentation layer has been created out of the instance of the model layer's class (init(layer:) call), within the stack of display method call before the draw(in ctx: CGContext) method call.

    So, assuming that presentation layer's class is the same as your custom layer's class, one way around that is to have the animated property different from the property that is being used for rendering of the layer's contents. You could animate interpolatedValue property, and have another dynamic property strokeEnd that will on the fly calculate appropriate value on every frame out of the interpolatedValue value.

    In the edge case, interpolatedValue could be just the animation progress from 0 to 1, and based on that you can dynamically calculate your strokeEnd value within your presentation layer. You can even ask your delegate about the value for a given progress:

    @objc @NSManaged open dynamic var interpolatedValue: CGFloat
    
    open override class func needsDisplay(forKey key: String) -> Bool {
        var result = super.needsDisplay(forKey: key)
        result = result || key == "interpolatedValue"
        return result
    }
     
    open var strokeEnd : CGFloat {
        get {
           return self.delegate.crazyLayer(self, strokeEndFor: self.interpolatedValue)
        }
        set {
            ...
        }
    }
    

    let animation = CABasicAnimation(keyPath: "interpolatedValue")
    animation.duration = 2.0
    animation.fromValue = 0.0
    animation.toValue = 1.0
    crazyLayer.add(animation, forKey: "interpolatedValue")
    

    Another way around that could be to use CAKeyframeAnimation where you can specify which value has to be assigned to the animatable property at a corresponding time.

    In case you might want to assign on each frame a complex value out of interpolated components, I have a project where I do that for animation of NSAttributedString value by interpolation of its attributes, and the article where I describe the approach.