I have added a custom property to a subclass of CALayer in order to animate the layer drawing. The property is correctly animated when I run a UIView.animate but NOT when running UIView.animateKeyframes. Any ideas? The full sample project is available here.
This kind of animation works:
UIView.animate(withDuration: self.duration, animations: {
self.transitionView.progress = 0
})
And that DOES NOT works:
UIView.animateKeyframes(withDuration: self.duration, delay: 0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1) {
self.transitionView.progress = 0
})
Here is the layer definition :
class AnimatedLayer: CALayer {
@NSManaged var progress: CGFloat
// Whenever a new presentation layer is created, this function is called and makes a COPY of the object.
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? AnimatedLayer {
progress = layer.progress
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override class func needsDisplay(forKey key: String) -> Bool {
if isAnimationKeySupported(key) {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if AnimatedLayer.isAnimationKeySupported(event) {
// Copy animation context and mutate as needed
guard let animation = currentAnimationContext(in: self)?.copy() as? CABasicAnimation else {
setNeedsDisplay()
return nil
}
animation.keyPath = event
if let presentation = presentation() {
animation.fromValue = presentation.value(forKeyPath: event)
}
animation.toValue = nil
return animation
}
return super.action(forKey: event)
}
private class func isAnimationKeySupported(_ key: String) -> Bool {
return key == #keyPath(progress)
}
private func currentAnimationContext(in layer: CALayer) -> CABasicAnimation? {
/// The UIView animation implementation is private, so to check if the view is animating and
/// get its property keys we can use the key "backgroundColor" since its been a property of
/// UIView which has been forever and returns a CABasicAnimation.
return action(forKey: #keyPath(backgroundColor)) as? CABasicAnimation
}
}
Ands the view code is :
class DayTransitionView: UIView {
override class var layerClass: AnyClass {
return AnimatedLayer.self
}
var progressLayer: AnimatedLayer {
return layer as! AnimatedLayer
}
@objc dynamic var progress: CGFloat {
set { progressLayer.progress = newValue }
get { return progressLayer.presentation()?.progress ?? progressLayer.progress }
}
// if not redefined draw:layer is not called !
override func draw(_ rect: CGRect) { }
override func draw(_ layer: CALayer, in ctx: CGContext) {
UIGraphicsPushContext(ctx);
// background color
let backgroundPath: CGPath = UIBezierPath(roundedRect: layer.bounds, byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight] , cornerRadii: .zero).cgPath
ctx.addPath(backgroundPath)
ctx.setFillColor(UIColor.green.cgColor)
ctx.closePath()
ctx.fillPath()
// background color
let fillHeight = self.progressive(layer.bounds.origin.y, layer.bounds.origin.y + layer.bounds.size.height)
let fillRect = CGRect(x: layer.bounds.origin.x, y: layer.bounds.origin.y + layer.bounds.size.height - fillHeight, width: layer.bounds.size.width, height: fillHeight)
let fillPath: CGPath = UIBezierPath(roundedRect: fillRect, byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight] , cornerRadii: .zero).cgPath
ctx.addPath(fillPath)
ctx.setFillColor(UIColor.red.cgColor)
ctx.closePath()
ctx.fillPath()
UIGraphicsPopContext();
}
func progressive(_ from: CGFloat, _ to: CGFloat) -> CGFloat {
return from + self.progress * (to - from)
}
}