Travis Griggs Travis Griggs - 1 year ago 51
iOS Question

Correct way to subclass CAShapeLayer

Inspired by this example, I have a created custom

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
you are limited by the clip of the layer's frame. With stock layers, you have the
which is by default
! But it seems once you the subclass route with drawing, that because implicitly and unchangeably

So I thought I would try a different approach, by subclassing
instead. And instead of adding a custom
, I would simply have my variables for
update the
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() {

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.addArcWithCenter(center, radius: radius, startAngle: self.start.radians.raw, endAngle: (self.start + self.sweep).radians.raw, clockwise: self.sweep >= 0.radians ? true : false)
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
statement there, I can see it interpolating through the values as expected during an animation. I have tried adding
in various locations, but to no avail.

Answer Source

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 (presentationLayer() as! WedgeLayer? ?? self).sweep to access the property.

    • Where using presentationLayer gives you the currently animated value
  • If you want implicit animation, implement actionForKey as described in this answer.