# Draw an arc based on a given percentage

I wanted to create a class that would allow me to draw an arc representing a percentage of progress, similar to the activity view on the Apple watch. There are various examples which I found helpful on the web and my output is a combination of many of these.

I have posted my code so that it works and renders in a Xcode playground. Could someone have a look over it and advise if i'm doing anything I shouldn't, or going wrong anywhere.

import UIKit
import XCPlayground

class CircleProgressView: UIView {

var startPoint: CGFloat = 0
var color: UIColor = UIColor.yellowColor()
var trackColor: UIColor = UIColor.grayColor()
var trackWidth: CGFloat = 1
var fillPercentage: CGFloat = 100

override init(frame: CGRect) {

super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()

} // init

required init(coder aDecoder: NSCoder) {

self.backgroundColor = UIColor.clearColor()

} // init

private func getGraphStartAndEndPointsInRadians() -> (graphStartingPoint: CGFloat, graphEndingPoint: CGFloat) {

// make sure our starting point is at least 0 and less than 100
if ( 0 > self.startPoint ) {
self.startPoint = 0
} else if ( 100 < self.startPoint ) {
self.startPoint = 100
} // if

// make sure our fill percentage is at least 0 and less than 100
if ( 0 > self.fillPercentage ) {
self.fillPercentage = 0
} else if ( 100 < self.fillPercentage ) {
self.fillPercentage = 100
} // if

// we take 25% off the starting point, so that a zero starting point
// begins at the top of the circle instead of the right side...
self.startPoint = self.startPoint - 25

// we calculate a true fill percentage as we need to account
// for the potential difference in starting points
let trueFillPercentage = self.fillPercentage + self.startPoint

let π: CGFloat = CGFloat(M_PI)

// now we can calculate our start and end points in radians
let startPoint: CGFloat = ((2 * π) / 100) * (CGFloat(self.startPoint))
let endPoint: CGFloat = ((2 * π) / 100) * (CGFloat(trueFillPercentage))

return(startPoint, endPoint)

} // func

override func drawRect(rect: CGRect) {

// first we want to find the centerpoint and the radius of our rect
let center: CGPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))
let radius: CGFloat = rect.width / 2

// make sure our track width is at least 1
if ( 1 > self.trackWidth) {
self.trackWidth = 1
} // if

// and our track width cannot be greater than the radius of our circle
if ( radius < self.trackWidth ) {
} // if

// we need our graph starting and ending points
let (graphStartingPoint, graphEndingPoint) = self.getGraphStartAndEndPointsInRadians()

// now we need to first draw the track...
var trackPath = UIBezierPath(arcCenter: center, radius: radius - (trackWidth / 2), startAngle: graphStartingPoint, endAngle: 2.0 * CGFloat(M_PI), clockwise: true)
trackPath.lineWidth = trackWidth
self.trackColor.setStroke()
trackPath.stroke()

// now we can draw the progress arc
var percentagePath = UIBezierPath(arcCenter: center, radius: radius - (trackWidth / 2), startAngle: graphStartingPoint, endAngle: graphEndingPoint, clockwise: true)
percentagePath.lineWidth = trackWidth
percentagePath.lineCapStyle = kCGLineCapRound
self.color.setStroke()
percentagePath.stroke()

return

} // func

} // class

var circleProgress: CircleProgressView = CircleProgressView()
circleProgress.frame = CGRectMake(0 , 0, 200, 200)

circleProgress.trackWidth = 20

circleProgress.startPoint = 0
circleProgress.fillPercentage = 75

XCPShowView("Container View", circleProgress)


I want to focus on just the getGraphStartAndEndPointsInRadians() method of your class, as this is the area I see the most problems with a cursory glance.

First of all, from the method name alone, I can tell it does too much. The word "and" should very rarely be used in method names. It's usage is usually a sign that a method does too much. And this one does.

First thing we can do is refactor out the percent-to-radians conversions into their own method.

func percentToRadians(percent: CGFloat) -> CGFloat {
return (2 * CGFloat(M_PI)) / 100) * percent
}


Next, this logic has no business in the method:

    // make sure our starting point is at least 0 and less than 100
if ( 0 > self.startPoint ) {
self.startPoint = 0
} else if ( 100 < self.startPoint ) {
self.startPoint = 100
} // if

// make sure our fill percentage is at least 0 and less than 100
if ( 0 > self.fillPercentage ) {
self.fillPercentage = 0
} else if ( 100 < self.fillPercentage ) {
self.fillPercentage = 100
} // if


Instead, this logic should go somewhere more closely related to the actual setting of these variables. Like in the didSet method.

A method with get in the name definitely shouldn't have side effects like this. (And we really don't use get in method names either.)

As an aside, all of your variables should have didSet methods that assure that the view is redrawn when their values are changed.

So, we're agreed that we'll move side-effecting logic out of this method, and that we'll refactor the percent-to-radian logic out, now we just need to split the methods. We need one for the start point and one for the end point. Why don't we do these as computer properties?

private var startingDrawPoint: CGFloat {
return percentToRadians(CGFloat(self.startPoint) - 25)
}

private var endingDrawPoint: CGFloat {
return percentToRadians(CGFloat(self.fillPercentage - self.startPoint) - 25)
}

• Thank you! I certainly appreciate your input. Your comments make a lot of sense and while they make the code generally larger, they are in the right place and make it easier to modify. Again, thanks for your help! – Jon Jul 22 '15 at 0:36