Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to subclass CAShapeLayer

Inspired by this example, I have a created custom CALayer subclasses for wedges and arcs. The allow me to draw arcs and wedges and animate changes in them so that they sweep radially.

One of the frustrations with them, is that apparently when you go this route of having a subclass drawInContext() you are limited by the clip of the layer's frame. With stock layers, you have the masksToBounds which is by default false! But it seems once you the subclass route with drawing, that because implicitly and unchangeably true.

So I thought I would try a different approach, by subclassing CAShapeLayer instead. And instead of adding a custom drawInContext(), I would simply have my variables for start and sweep update the path of the receiver. This works nicely BUT it will no longer animate as it used to:

import UIKit

class WedgeLayer:CAShapeLayer {
    var start:Angle = 0.0.rotations { didSet { self.updatePath() }}
    var sweep:Angle = 1.0.rotations  { didSet { self.updatePath() }}
    dynamic var startRadians:CGFloat { get {return self.start.radians.raw } set {self.start = newValue.radians}}
    dynamic var sweepRadians:CGFloat { get {return self.sweep.radians.raw } set {self.sweep = newValue.radians}}

    // more dynamic unit variants omitted
    // we have these alternate interfaces because you must use a type
    // which the animation framework can understand for interpolation purposes

    override init(layer: AnyObject) {
        super.init(layer: layer)
        if let layer = layer as? WedgeLayer {
            self.color = layer.color
            self.start = layer.start
            self.sweep = layer.sweep
        }
    }

    override init() {
        super.init()
    }

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

    func updatePath() {
        let center = self.bounds.midMid
        let radius = center.x.min(center.y)
        print("updating path \(self.start) radius \(radius)")

        if self.sweep.abs < 1.rotations {
            let _path = UIBezierPath()
            _path.moveToPoint(center)
            _path.addArcWithCenter(center, radius: radius, startAngle: self.start.radians.raw, endAngle: (self.start + self.sweep).radians.raw, clockwise: self.sweep >= 0.radians ? true : false)
            _path.closePath()
            self.path = _path.CGPath
        }
        else {
            self.path = UIBezierPath(ovalInRect: CGRect(around: center, width: radius * 2, height: radius * 2)).CGPath
        }
    }

    override class func needsDisplayForKey(key: String) -> Bool {
        return key == "startRadians" || key == "sweepRadians" || key == "startDegrees" || key == "sweepDegrees" || key == "startRotations" || key == "sweepRotations" || super.needsDisplayForKey(key)
    }
}

Is it not possible to make it regenerate the path and update as it animates the value? With the print() statement there, I can see it interpolating through the values as expected during an animation. I have tried adding setNeedsDisplay() in various locations, but to no avail.

like image 391
Travis Griggs Avatar asked Nov 10 '22 03:11

Travis Griggs


1 Answers

There are a couple tricks to getting this to work properly:

  • Don't use the didSet. Rather, you should override display() and update self.path there.

  • Use (presentation() as! WedgeLayer? ?? self).sweep to access the property. (presentation, known as presentationLayer in Obj-C and Swift 2, allows you to access the currently-visible property values during animation.)

  • If you want implicit animation, implement actionForKey as described in this answer.

like image 172
jtbandes Avatar answered Nov 15 '22 13:11

jtbandes