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.)
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.