Search code examples
swiftcore-animationcalayer

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)")
   }

}

Solution

  • 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.