Search code examples
iosswiftcore-graphicscore-animation

Swift 3: CAShapeLayer mask not animating for pie chart animation


I have code that successfully creates a pie chart given arbitrary input data. It produces a pie chart that looks like this:

Pie chart to animate

What I want:

I wish to create an animation that draws those pieces of the pie chart in a sequential order. That is, I wish to perform a "clock swipe" animation of the pie chart. The screen should start of black and then gradually fill to produce the above image of the pie chart.

The problem:

I am trying to create a layer mask for each pie chart piece and animate the layer mask so that the pie chart piece fills in an animation. However, when I implement the layer mask, it does not animate and the screen remains black.

The code:

class GraphView: UIView {
    // Array of colors for graph display
    var colors: [CGColor] = [UIColor.orange.cgColor, UIColor.cyan.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor]

    override func draw(_ rect: CGRect) {
        drawPieChart()
    }

    func drawPieChart() {
        // Get data from file, yields array of numbers that sum to 100
        let data: [String] = getData(from: "pie_chart")

        // Create the pie chart
        let pieCenter: CGPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
        let radius = frame.width / 3
        let rad = 2 * Double.pi
        var lastEnd: Double = 0
        var start: Double = 0

        for i in 0...data.count - 1 {
            let size = Double(data[i])! / 100
            let end: Double = start + (size * rad)

            // Create the main layer
            let path = UIBezierPath()
            path.move(to: pieCenter)
            path.addArc(withCenter: pieCenter, radius: radius, startAngle: CGFloat(start), endAngle: CGFloat(end), clockwise: true)

            let shapeLayer = CAShapeLayer()
            shapeLayer.path = path.cgPath
            shapeLayer.fillColor = colors[i] 

            // Create the mask
            let maskPath = CGMutablePath()
            maskPath.move(to: pieCenter)
            maskPath.addArc(center: pieCenter, radius: radius, startAngle: CGFloat(start), endAngle: CGFloat(end), clockwise: true)

            let shapeLayerMask = CAShapeLayer()
            shapeLayerMask.path = maskPath
            shapeLayerMask.fillColor = colors[i]
            shapeLayerMask.lineWidth = radius
            shapeLayerMask.strokeStart = 0
            shapeLayerMask.strokeEnd = 1

            // Set the mask
            shapeLayer.mask = shapeLayerMask
            shapeLayer.mask!.frame = shapeLayer.bounds

            // Add the masked layer to the main layer
            self.layer.addSublayer(shapeLayer)

            // Animate the mask
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
                let animation = CABasicAnimation(keyPath: "strokeEnd")
                animation.fromValue = shapeLayerMask.strokeStart
                animation.toValue = shapeLayerMask.strokeEnd
                animation.duration = 4
                animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
                animation.isRemovedOnCompletion = false
                animation.autoreverses = false

                shapeLayerMask.add(animation, forKey: nil)
            }

            start = end
        }
    }
}

Again, this code just yields a blank screen. If you comment out the mask related code and add shapeLayer as the subview, you will see the un-animated pie chart.

What am I doing wrong and how can I get my pie chart to animate?


Solution

  • The sublayers must be added to a parent layer before they can be masked:

    // Code above this line adds the pie chart slices to the parentLayer
    for layer in parentLayer.sublayers! {
        // Code in this block creates and animates a mask for each layer
    }
    // Add the parent layer to the highest parent view/layer
    

    That's really all there is to it. You must ensure that a one-to-one correspondence exists between pie chart slices and sublayers. The reason for this is because you cannot mask multiple views/layers with one instance of a layer mask. When you do this, the layer mask becomes part of the Layer Hierarchy of the layers that you are trying mask. A parent-child relationship between the mask and the layer to-be-masked must be established for this to work.