3
\$\begingroup\$

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) {

        super.init(coder: aDecoder)
        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 ) {
            self.trackWidth = radius
        } // 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)
\$\endgroup\$
4
\$\begingroup\$

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)
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ 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! \$\endgroup\$ – Jon Jul 22 '15 at 0:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.