0
votes

I have a CALayer with a custom animation on it, working via an @NSManaged property, and overriding:

  • class func defaultValue(forKey key: String) -> Any?
  • class func needsDisplay(forKey key: String) -> Bool
  • func action(forKey key: String) -> CAAction?
  • func display()

However, I sometimes want to bypass the animation and have the property immediately step to the new value. In my CALayer sub-class I tried this:

@NSManaged private var radius: CGFloat

func animate(to radius: CGFloat) {
    self.radius = radius
}

func step(to radius: CGFloat) {
    // Inspired by https://stackoverflow.com/a/34941743
    CATransaction.begin()
    CATransaction.setDisableActions(true)   // Prevents animation occurring
    self.radius = radius
    CATransaction.commit()
    setNeedsDisplay()                       // Has no effect
}

When animate(to:) is called, display() is called repeatedly and my custom drawing code can do it's thing. When step(to:) is called, the CATransaction code does prevent an animation from occurring, but no drawing is ever performed at all.

I can get it to behave as desired, but it feels quite hacky:

func step(to radius: CGFloat) {
    // func action(forKey key: String) -> CAAction? uses .animationDuration
    // when constructing a CABasicAnimation
    let duration = animationDuration
    defer { animationDuration = duration }
    animationDuration = 0
    self.radius = radius
}

What is the correct method to give the caller the ability to choose whether the property animates from one value to the next, or steps immediately? A subsequent change to radius should respect the previous value, whether it was stepped or animated to.

1

1 Answers

1
votes

You say you have implemented action(forKey:) (quite rightly). So on those occasions when you don't want this property to be animated, return nil from that method. The drawing will still take place, but without animation.

Alternatively, you could return super.action(forKey:key). That might be a little more sane, but the outcome is the same.


You may ask (and I hope you do): How can I throw some kind of switch that action(forKey:) can consult in order to know which kind of occasion this is? One possibility is to set a property of the layer using key-value coding.

CALayer has a wonderful feature that you are allowed to call setValue(_:forKey:) or value(forKey:) for any key; it doesn't have to be a "real" key that already exists.

So you could call setValue(false, forKey:"shouldAnimate") on the layer before setting the property. And your action(forKey:) can then consult value(forKey:"shouldAnimate"), and see whether it is false (as opposed to true or nil) — and if it is, it returns nil to prevent the animation.