Alex Bollbach Alex Bollbach - 14 days ago 7
Swift Question

Subclassing CALayer + Implicit Animation w/ Swift3

I am trying to implement "implicit animations" by overriding CALayer. I've read through the documentation and find it rather complicated. More over, I can't find any decent code examples, or internet posts explaining it well or using any Swift.

I've implemented by subclass

BandBaseCG
which attempts to provide implicit animation for property
param1
, and overridden
needsDisplay(forKey:)
which does get called once with my property where I return true.

But here is where it goes wrong. My understanding is that I must return an object conforming to the
CAAction
protocol, say a
CABasicAnimation
, and this is what I do. However,
action(forKey:)
or
defaultAction(forKey:)
do not get called with the
param1
key.

So I understand that at some point I can provide Core-Graphics drawing code in
draw(in ctx:)
to implement the actual implicit animation in response to some
layer.param1 = 2
. But I just cannot get to this point because the API is simply not working how I expected.

Here is my current implementation:

import Foundation
import QuartzCore

class BandBaseCG: CALayer {

@NSManaged var param1: Float

override init(layer: Any) {

super.init(layer: layer)

}

override init() {
super.init()

}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


override func action(forKey event: String) -> CAAction? {

print(event)

if event == "param1" {

let ba = CABasicAnimation(keyPath: event)
ba.duration = 2
ba.fromValue = (presentation()! as BandBaseCG).param1
ba.toValue = 2

return ba
}

return super.action(forKey: event)
}


override func draw(in ctx: CGContext) {
print("draw")
}


override class func defaultAction(forKey event: String) -> CAAction? {

if event == "param1" {

let ba = CABasicAnimation(keyPath: event)
ba.duration = 2
ba.fromValue = 0
ba.toValue = 2

return ba
}

return super.defaultAction(forKey: event)
}

override class func needsDisplay(forKey key: String) -> Bool {

print(key)

if key == "param1" {
return true
}

return super.needsDisplay(forKey: key)
}

override func display() {
print("param value: \(param1)")
}

}

Answer

Here's a complete working example (from my book):

class MyView : UIView { // exists purely to host MyLayer
    override class var layerClass : AnyClass {
        return MyLayer.self
    }
    override func draw(_ rect: CGRect) {} // so that layer will draw itself
}
class MyLayer : CALayer {
    @NSManaged var thickness : CGFloat
    override class func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(thickness) {
            return true
        }
        return super.needsDisplay(forKey:key)
    }
    override func draw(in con: CGContext) {
        let r = self.bounds.insetBy(dx:20, dy:20)
        con.setFillColor(UIColor.red.cgColor)
        con.fill(r)
        con.setLineWidth(self.thickness)
        con.stroke(r)
    }
    override func action(forKey key: String) -> CAAction? {
        if key == #keyPath(thickness) {
            let ba = CABasicAnimation(keyPath: key)
            ba.fromValue = self.presentation()!.value(forKey:key)
            return ba
        }
        return super.action(forKey:key)
    }
}

You will find that if you put a MyView into your app, you can say

let lay = v.layer as! MyLayer
lay.thickness = 10 // or whatever

and that it will animate the change in the border thickness.