Correct way to subclass CAShapeLayer
Asked Answered
T

1

6

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.

Toothless answered 6/10, 2015 at 0:39 Comment(0)
H
1

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.

Hoopoe answered 6/10, 2015 at 5:19 Comment(1)
Awesome and succint answer! Did the first two bullets and it worked. Could I beg you to add some explanation of why this approach works, and mine didn't? Especially what the second bullet point is doing. It's obviously overcoming something the first causes. When I did just the first, I noticed via my print() statement that the values weren't changing as it animated through (which was a regression from what I was seeing with my original approach).Toothless

© 2022 - 2024 — McMap. All rights reserved.