Ben Pious
Ben Pious

Reputation: 4805

CALayer.draw(in:) and CALayer.needsDisplay(forKey:) not called when expected, presentation layer unexpectedly nil

I'm trying to implement a layer with implicitly animated properties, and I am seeing some very strange behavior. Here's a simple layer that demonstrates what I mean:

class CustomLayer: CALayer {

    override init() {
        super.init()
        implcitlyAnimatedProperty = 0.0000
        needsDisplayOnBoundsChange = true
    }

    override init(layer: Any) {
        super.init(layer: layer)
        implcitlyAnimatedProperty = (layer as! CustomLayer).implcitlyAnimatedProperty
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    @NSManaged
    var implcitlyAnimatedProperty: CGFloat

    override func action(forKey event: String) -> CAAction? {
        if event == "implcitlyAnimatedProperty" {
            let action = CABasicAnimation(keyPath: event)
            action.fromValue = presentation()?.value(forKey: event) ?? implcitlyAnimatedProperty
            return action
        } else {
            return super.action(forKey: event)
        }
    }

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

    override func draw(in ctx: CGContext) {
        if presentation() == nil {
            print("presentation is nil")
        }
        print(presentation()?.value(forKey: "implcitlyAnimatedProperty") ?? implcitlyAnimatedProperty)
    }
}

I create an instance of CustomLayer and attempt to animate the property implicitly like this:

        let layer = CustomLayer()
        view.layer.addSublayer(layer) // I'm doing this in a view controller, when a button is pressed
        CATransaction.begin()
        CATransaction.setDisableActions(false)
        CATransaction.setAnimationDuration(10)
        layer.implcitlyAnimatedProperty = 10 // (1)
        layer.bounds = CGRect(x: 0, y: 0, width: 100, height: 100) // (2)
        CATransaction.commit()

The behavior that I'm seeing is as follows:

A common explanation for these functions not being called is that the layer has a delegate which is handling these calls on its behalf. But the layer's delegate is nil here, so that can't be what's happening.

Why is this happening, and how can I fix it?

Upvotes: 2

Views: 763

Answers (1)

clemens
clemens

Reputation: 17722

Your layer isn't drawn, because it has empty bounds and is invisible. You should move (2) before the transaction instead of removing it.

Some notes to your code:

You shouldn't write initializers to set your properties. Overwrite defaultValue(forKey:) instead:

override class func  defaultValue(forKey key: String) -> Any? {
    if key == "implcitlyAnimatedProperty" {
        return 0.0
    }
    else {
        return super.defaultValue(forKey: key)
    }
}

The setters of layer properties have some surprising features and side effects. E.g. when you set a property inside of a transaction the method action(forKey:) is called before the value is applied to the properties. Thus you may simplify the line

action.fromValue = presentation()?.value(forKey: event) ?? implcitlyAnimatedProperty

to

action.fromValue = implcitlyAnimatedProperty

presentation() may return nilin draw(in:), because self may be the presentation layer of your (model) layer. Check model() it will return the layer you have created.

needsDisplay(forKey:) is a class method, it is called just once for each property. Core Animation decides only once for all layer instances, if a property is animatable.

Upvotes: 3

Related Questions