Search code examples
iosswiftcore-graphicsuibezierpath

Create a circle with multi coloured segments in Core Graphics


I am trying to draw a pie chart which will be made up of equal sized segments, each with a different colour. I am basing my code off this SO: Draw a circular segment progress in SWIFT

let circlePath = UIBezierPath(ovalInRect: CGRect(x: 200, y: 200, width: 150, height: 150))
var segments: [CAShapeLayer] = []
let segmentAngle: CGFloat = 1.0 / CGFloat(totalSegments)

for var i = 0; i < totalSegments; i++ {
    let circleLayer = CAShapeLayer()
    circleLayer.path = circlePath.CGPath

    // start angle is number of segments * the segment angle
    circleLayer.strokeStart = segmentAngle * CGFloat(i)

    //create a gap to show segments
    let gapSize: CGFloat = 0.008
    circleLayer.strokeEnd = circleLayer.strokeStart + segmentAngle - gapSize

    circleLayer.lineWidth = 10
    circleLayer.strokeColor = UIColor(red:0,  green:0.004,  blue:0.549, alpha:1).CGColor
    circleLayer.fillColor = UIColor.redColor().CGColor

    // add the segment to the segments array and to the view
    segments.insert(circleLayer, atIndex: i)
    view.layer.addSublayer(segments[i])
}

But using this I wanted to make it so I can colour each segment based on the index value i in the for loop. Initially I just tested with odd or even, but found the fill part would fill the entire circle with the last color used.

I suspect that is because the fill colour fills the entire circle, even though the stroke has a finite start and end. How could I create this same effect but enable separate fill color for each segment?


Solution

  • Here's a pie chart drawing function I had fun creating in the playground. Perhaps you can use it for your requirements:

     import Foundation
     import UIKit
    
    
     // Pie Chart Drawing function
     // --------------------------
     // - Takes an array of tuples with relative value and color for slices
     // - draws at center coordinates with a givn radius
     //
     func drawPieChart(slices:[(value:CGFloat, color:UIColor)], at center:CGPoint, radius:CGFloat)
     {  
         // the calue component of the tuples are summed up to get the denominator for surface ratios
         let totalValues:CGFloat = slices.reduce(0, combine: {$0 + $1.value})
    
         // starting at noon (-90deg)
         var angle:CGFloat = -CGFloat(M_PI)/2
    
         // draw each slice going counter clockwise
         for (value,color) in slices
         {
            let path = UIBezierPath()
    
            // slice's angle is proportional to circumference 2π 
            let sliceAngle = CGFloat(M_PI)*2*value/totalValues 
            // select fill color from tuple  
            color.setFill()
    
            // draw pie slice using arc from center and closing path back to center
            path.moveToPoint(center)
            path.addArcWithCenter( center, 
                           radius: radius, 
                       startAngle: angle, 
                         endAngle: angle - sliceAngle, 
                        clockwise: false
                                 )
            path.moveToPoint(center)
            path.closePath()
    
            // draw slice in current context
            path.fill()
    
            // offset angle for next slice
            angle -= sliceAngle
         }
     }
    
     // Pie chart Data
     let slices:[(value:CGFloat, color:UIColor)] =
       [ (3, UIColor.greenColor()),
         (5, UIColor.redColor()),
         (8, UIColor.blueColor()),
         (13,UIColor.yellowColor())
       ]  
    
     // UIView
     let viewSize    = CGSize(width: 300, height: 300)
     let view:UIView = UIView(frame: CGRect(origin: CGPointZero, size: viewSize))
     view.backgroundColor = UIColor.whiteColor()
    
     // CoreGraphics drawing
     UIGraphicsBeginImageContextWithOptions(viewSize, false, 0)
    
     // draw pie chart in Image context
     drawPieChart(slices, at:CGPointMake(150,150), radius:100 )
    
     // set image to view layer (you could also use it elsewhere)
     view.layer.contents = UIGraphicsGetImageFromCurrentImageContext().CGImage
     UIGraphicsEndImageContext()
    
     // Playground (quick look or Timeline)
     let preview = view
    

    Note: to get equal size segments, just provide the same value in all entries of the tuple array.

    UPDATE

    Here's a variant of the function that draws only the perimeter (rim) of the pie chart:

     func drawPieRim(_ slices:[(value:CGFloat, color:UIColor)], at center:CGPoint, 
                         radius:CGFloat, thickness:CGFloat=30)
     {  
         let totalValues:CGFloat = slices.reduce(0){$0 + $1.value}
         let gapAngle      =  CGFloat(Double.pi) * 0.01
         var angle:CGFloat =  -CGFloat(Double.pi)/2
         for (value,color) in slices
         {
            let path       = UIBezierPath()
            let sliceAngle = CGFloat(Double.pi)*2*value/totalValues
            path.lineWidth = thickness
            color.setStroke()
            path.addArc( withCenter:center, 
                           radius: radius, 
                       startAngle: angle + sliceAngle - gapAngle, 
                         endAngle: angle, 
                        clockwise: false)
            path.stroke()
            angle += sliceAngle 
         }
     }