Reputation: 4805
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:
draw(in:)
is called, presentation()
returns nil
, which, in my real app, makes the layer draw once with the final values from the model layer, which is an undesirable visual artifact. Otherwise, everything works as expected.needsDisplay(forKey:)
is never called after the layer is created, and draw(in:)
is never called. action(forKey:)
is called, however. 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
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 nil
in 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