How can I apply a shadow to each slice while maintaining the current animation? I have tried putting it on the layer itself, but the shadow cannot be seen. I also tried creating another layer with a clear background, but with a clear background, the shadow does not work.
override func draw(_ rect: CGRect) {
super.draw(rect)
layer.sublayers?.filter { $0 is CAShapeLayer }.forEach { $0.removeFromSuperlayer() }
let totalValue = data.reduce(0.0) { $0 + $1.value }
let center = CGPoint(x: rect.midX, y: rect.midY)
var startAngle: CGFloat = -CGFloat.pi / 2
var cumulativeDelay: CFTimeInterval = 0
gapSizeDegrees = data.count == 1 ? 0 : gapSizeDegrees
data.forEach { value in
let segmentValue = CGFloat(value.value)
let color = value.color
let endAngle = startAngle + (segmentValue / CGFloat(totalValue)) * 2 * .pi
let outerRadius = rect.width / 2
let outerStartAngle = startAngle + gapSizeDegrees.toRadians()
let outerEndAngle = endAngle - gapSizeDegrees.toRadians()
let path = UIBezierPath(arcCenter: center, radius: outerRadius, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: true)
let innerRadius = outerRadius - strokeWidth
path.addArc(withCenter: center, radius: innerRadius, startAngle: outerEndAngle - gapSizeDegrees / rect.width, endAngle: outerStartAngle + gapSizeDegrees / rect.width , clockwise: false)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = color.cgColor
shapeLayer.strokeColor = color.cgColor
layer.addSublayer(shapeLayer)
animateSliceFilling(sliceLayer: shapeLayer, center: center, radius: outerRadius, innerRadius: innerRadius, startAngle: startAngle, endAngle: endAngle, delay: cumulativeDelay)
cumulativeDelay += Consts.Appearence.animationDuration / Double(data.count)
startAngle = endAngle
}
}
private func animateSliceFilling(sliceLayer: CAShapeLayer, center: CGPoint, radius: CGFloat, innerRadius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, delay: CFTimeInterval) {
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath(arcCenter: center, radius: (radius + innerRadius) / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
maskLayer.path = maskPath.cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = strokeWidth
maskLayer.strokeEnd = 0.0
sliceLayer.mask = maskLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = Consts.Appearence.animationDuration / Double(data.count)
animation.beginTime = CACurrentMediaTime() + delay
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
maskLayer.add(animation, forKey: "strokeEndAnimation")
}
A few observations:
I would put the shadow on its own layer (in case the shadow is greater than the gap angle; you want to make sure that one layer’s shadow does not overlay another layer’s data).
I would create shape layers for the shadow layer; and each of the data layers.
I would then animate the mask on the whole parent layer.
We should not add layers or perform animation from
draw(rect:). (This is used when rending your own animation and is for rendering a single frame; but we are relying on CoreGraphics to do the animation for us, sodraw(rect:)is simply not the right place to do this animation.Using
layoutSubviewswould be more logical.As an aside, whenever you do implement
draw(rect:)(which we will not do here), never assume thatrectrepresents the full view. It represents what portion of the view that is being drawn. Usebounds, instead.Personally, I would calculate the delta for each arc, and then inset the start and end angles for each by one half of the gap radius.
I would be inclined to render each arc as a single arc (not an inner in one direction and an outer arc in the other) unless you absolutely needed that behavior (e.g., you wanted to do some corner rounding of the arcs, themselves). It is not critical to this question, but simplifies the code a bit.
I would calculate the radius from the minimum of the width and height.
That yields:
If you want the gaps between the arcs to be parallel and you want the corners rounded, you could do something like:
See first bullet point under Set specific value to round corners of UIBezierPath for a visualization of how I am rendering the rounded corners.
Anyway, that uses this
UIBezierPathextension:That yields:
(Note, I exaggerated the width of these arcs so that the parallel spacing can clearly be seen.)