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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With