Swift Swift: Basic Core Graphics for the Ring Graph

Screenshot 2015-03-10 15.42.16If you take a look at many of the cutting-edge designs for mobile User interfaces on Behance or Pinterest, you find a ring, arc or circle graph. The Apple Watch’s fitness activity app uses them extensively. Yet, if you scroll down the object library in Xcode it is a not a control to drag and drop into Xcode. You have to make it yourself.

Actually, it’s not difficult to draw your own ring graph using Core graphics. In this post, we’ll walk through the process of making and using an arc graph.

Create the Project

Start by making a new Single-view project by pressing Command-Shift-N on the keyboard or File>New>Project.. from the drop down menu. Name the project SwiftRingGraph. Use Swift as the language and make it a Universal app.  Save the file somewhere convenient.

Go to the storyboard. Drag three sliders, two labels and a view (not a view controller) onto the scene.  Make both labels 20 point. Change one label to Pizza Power with a background color of white. Change the other label to 0% with a background color to Light gray . Make the view have a background color of white.  Make the background of one slider yellow, one slider red, and one slider white. Make the scene’s background black. Drag and size the objects to look like this:

Screenshot 2015-03-10 14.31.29

Add Auto Layout

You can leave it like this if you want to run in the iPad simulator.  I’ll add some auto layout to let this work on any device. If you haven’t used auto layout before, I suggest reading the Basic Auto Layout post or viewing the video, but you can follow along even if you  don’t.

Select the Pizza Power label and using the pin menu, pin 0 up -16 left, 5 right and 10 down. Also set the height to 50 points like this:

Screenshot 2015-03-10 14.32.12

Select the 0% label. Pin up 0 and right 0 points. Add the constraints.  Control drag from the 0% label to the Pizza Power Label.  In the menu that appears, select Equal Widths and Equal Heights.

Screenshot 2015-03-10 06.24.28

Pin the yellow slider up 20, left 0 and  right 0  points. Pin the white slider up 5, left 0 and right 0 points. Pin the red slider up 5, left 0,and right 0 points.

Select the View. Click the pin menu and set it for 300 width and 300 points height. Click the alignment menu and check on Center Horizontally, making sure the values are 0 points.

Screenshot 2015-03-10 14.46.24

In the resolver, select Update Frames.

Screenshot 2015-03-10 06.30.07

Select the Pizza Power Label. In the Equal width to 0% constraint, click Edit.

Screenshot 2015-03-10 14.52.42

Change the Multiplier from 1 to 1:3 or 3:1 so you get this on the storyboard:

Screenshot 2015-03-10 14.50.48

Connect the Outlets and Actions

Press Command-N. Add a Cocoa Touch Class called CircleGraphView subclassing UIView with Swift as the language.  Save the file and go back to the storyboard. Select the view. In the identity inspector, change the Custom Class to CircleGraphView:

Screenshot 2015-03-10 06.45.05

Close the inspector panel to give yourself room. Open up the Assistant editor. Select the 0% Label. Control drag from the label to the view controller class. Name the label’s outlet percentLabel. Control drag from the view to the viewController class. Name it circleGraph. Control drag from the slider to the code. Make an action named slider with a sender of UISlider. You should have this in your code:

    @IBOutlet weak var percentLabel: UILabel!
    @IBOutlet weak var circleGraph: CircleGraphView!
    @IBAction func slider(sender: UISlider) {
    }

Coding the Arc Graph View

Close the assistant editor and open the CircleGraphView.swift file. You will find some commented out code with a dire warning. We are doing custom drawing, so go ahead and remove the comments so you have this:

class CircleGraphView: UIView {

    override func drawRect(rect: CGRect) {
        // Drawing code
    }

}

Add the following properties to the code.

var endArc:CGFloat = 0.0 // in range of 0.0 to 1.0
var arcWidth:CGFloat = 10.0
var arcColor = UIColor.yellowColor()
var arcBackgroundColor = UIColor.blackColor()

We have the property endArc which is where the arc ends. We have a property arcWidth which will tell us how wide a line we want, with a default value of 10. Both values are CGFloat. Everything has a type CGFloat in Core Graphics, so keeping our properties typed correctly saves us a lot of work later. We also added  two color properties to set the color of the graph, and gave them some default values.

Working with Radians

Drawing always requires some math and geometry. Add the following to the drawRect method.

//Important constants for circle
        let fullCircle = 2.0 * CGFloat(M_PI)
        let start:CGFloat = -0.25 * fullCircle
        let end:CGFloat = endArc * fullCircle + start

Ring or arc graphs are circular arcs drawn in the view. We need a center point, an angle on the circle to start and an angle on the circle to end.
Core graphics does not work in degrees for angles, but radians. If you don’t remember your geometry or trigonometry lessons, radians describes angles around a circle as a measurement of pi. A full revolution around a circle is 2 times pi. There is a Double constant M_PI in Xcode we can use for calculations. We would need to convert it to CGFloat and multiply by 2 every time we use it. I calculated it once as a constant to use in calculations later. Looking at line 3 above, you’ll see how handy this is.  Any point on the circle is a fraction of a whole circle. If I use a CGFloat between 1 and 0, multiplied by fullCircle, I can describe any angle on the circle. Line 3 sets start by moving backwards a quarter of a circle. We do this because people think in clocks. The starting point should be 12 o’clock. The starting point of 0 in Core Graphics is at the 3 o’clock position, so we move back a quarter to start our line at the top.
Line four multiplies the endArc property by our full circle constant giving us where the arc ends. Since we shifted the arc a quarter of a turn we add that as an offset.

Next, Add this code:

//find the centerpoint of the rect
        var centerPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))

Core Graphics has a set of functions to get dimension information from a CGRect rectangle. Two of these are CGRectGetMidX() and CGRectGetMidY(). I could type in these functions every time I need them, but I find having a CGPoint tends to be useful and better documentation.
Next add these lines of code:

       //define the radius by the smallest side of the view
        var radius:CGFloat = 0.0
        if CGRectGetWidth(rect) > CGRectGetHeight(rect){
            radius = (CGRectGetWidth(rect) - arcWidth) / 2.0
        }else{
            radius = (CGRectGetHeight(rect) - arcWidth) / 2.0
        }

The size of our circle or arc is set by the radius. We are going to automatically fit a circle into the view, based on the smallest side of the view and the width of the arc. We first figure if the width or length is shorter. We take that shorter side. We subtract the width of the line, so it does not go outside of the view, clipping it. We divide that by two, since we need a radius. We now have a line that will always fit inside any size view.

Starting to Draw

Before drawing, you must have a context to draw with. There is a core graphics function to do this UIGraphicsGetCurrentContext. Assign its value to a constant like this:

//starting point for all drawing code is getting the context.
let context = UIGraphicsGetCurrentContext()

Once you have a context, set the color space. For most purposes just use RGB like this:

        //set colorspace
        let colorspace = CGColorSpaceCreateDeviceRGB()

After adding those lines of code, set your line attributes. Add the following code:

//set line attributes
CGContextSetLineWidth(context, arcWidth)
CGContextSetLineCap(context, kCGLineCapRound)
CGContextSetStrokeColorWithColor(context, arcColor.CGColor)

All the functions require the context. Line 2 sets the line width and line 3 the end caps on the line. Since we are drawing a line, we have a few choices what the end of the line looks like. In this case we use the constant kCGLineCapRound to make a round end for the line, just like the Apple Watch uses. There is also kCGLineCapButt and kCGLineCapSquare for straighter lines. If kCGLineCapRound produces a syntax error, you may be on Swift 2.0. Use GCLineCap.Round  or .Round instead if it doesn’t work. The version with k‘s are for Objective-C.

The last line sets the line color. Core graphics does not use UIColor. However, UIColor has a property which is a CGColor, which is what we do here.

After all that, we draw the line. Add this to the code:

CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, start, end, 0)
CGContextStrokePath(context)

Line 1 adds an arc to the path. Paths are invisible, and may have several lines, arcs,  and curves added to them. We have only one arc. End the path and draw the line from the path with the CGContextStrokePath function.

That is all we need to draw one arc in a view. We now have to tell the system to draw it. It is tempting to call the drawRect method from our view controller but there is a rule about Core Graphics: Never call drawRect. It will mess up the system if you do. Instead we call the view’s method setNeedsDisplay. How we call it is another question. For something like this, I’d call it from a change in  properties.  Change the code on the top of the class like this:

var endArc:CGFloat = 0.0{   // in range of 0.0 to 1.0
    didSet{
         setNeedsDisplay()
    }
}

We used a property observer to call setNeedsDisplay any time we change the endArc value.

Go to the ViewController.swift file. In the slider method, change to this:

@IBAction func slider(sender: UISlider) {
    circleGraph.endArc = CGFloat(sender.value)
    percentLabel.text = String(format:" %5.2f %%",sender.value * 100)
}

Every time we change the arc length in the slider, we run the code. We set value of the slider as a percentage by multiplying by 100. Build and run.

Screenshot 2015-03-10 15.03.13

It looks good, but the arc does not show up until you move the slider. We could do several things to fix this. We could set the slider’s initial value to 0. We could make an outlet for the slider, get its value and set it in viewDidLoad. For a short solution in a long post, I’ll hack the solution(I’d add the outlet in a real app). We know that the slider will have a value of 0.5. so in viewDidLoad set this:

circleGraph.endArc = 0.5

The line is a bit thin for the space. also in viewDidLoad, add code to make a bigger line:.

circleGraph.arcWidth = 35.0

Build and run. We now start with an arc on the device.

Screenshot 2015-03-10 15.04.52

Adding a Circle Background

You will find in most applications, like the activity monitor on the Apple Watch a faint circle background behind the arc. It give a user a sense of the track  of your arc. Between the CGContextSetLineCap and CGContextSetStrokeColorWithColor add this code:

//make the circle background
CGContextSetStrokeColorWithColor(context, arcBackgroundColor.CGColor)
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, fullCircle, 0)
CGContextStrokePath(context)

This makes a dark circle behind the yellow arc. Drawing happens from the bottom of the view up, We need to place this code before the code for the arc in order for it to behind the arc. I changed the start and end for 0 and fulCcircle, creating a full circle.

Back in viewController, in viewDidLoad, set the property arcBackgroundColor to make a very dark gray track.

let backgroundTrackColor = UIColor(white: 0.15, alpha: 1.0)
circleGraph.arcBackgroundColor = backgroundTrackColor

Build and run.

Screenshot 2015-03-10 15.07.15

One more change to make is the yellow arc’s width be slightly smaller than the track. Add this line before just after the second CGContextSetStrokeColorWithColor:

CGContextSetLineWidth(context, lineWidth* 0.8)

This makes the yellow line only 80% of the line width, leaving a small remnant of the track. Build and run:

Screenshot 2015-03-10 15.11.39

Adding More Views

Let’s add some nested arcs like the activity graph for the Apple watch on your phone or iPad .

Move the slider over to about a third of the screen. Add two more sliders in line with the current slider so they line up like this:

Change the current view’s background color to yellow. Now drag a view out on the storyboard directly over the current view keep the background  of the new view white. Resize the new view so you can see the yellow of the first view. The white view, when placed on teh yellow view is now a subview of the yellow view.

Screenshot 2015-03-10 15.16.22

Control drag diagonally up and left until it highlights the current view. Shift-Select Center X, Center Y, Equal Heights and Equal Widths. Keeping the new view selected, Go to the size inspector. Edit the constraints so they all have 0 for a constant.

Screenshot 2015-03-10 15.21.08

Update the constraints for the frame. It will fill in the square.  Change the Equal Width and Equal Height to a multiplier of 2:3 or 3:2 whichever makes a smaller view.

Screenshot 2015-03-10 15.24.09

We have nested the white view inside the yellow view. Repeat these steps to nest a smaller red view inside the white view. You should end up with this.

Screenshot 2015-03-10 15.28.15

 

Select the white view.  in the identity inspector, change the class to CircleGraphView. Select the red view. Change the class to CircleGraphView.

Open the assistant editor and add actions for sliderWhite for the white slider and sliderRed for the red slider. Add and outlet whiteCircle  for the white view and redCircle for the red view.

In the sliderWhite code add this:

whiteCircle.endArc = CGFloat(sender.value)

In the sliderRed code add this:

redCircle.endArc = CGFloat(sender.value)

In viewDidLoad add this:

whiteGraph.arcWidth = 25.0
whiteGraph.arcColor = UIColor.whiteColor()
whiteGraph.endArc = 0.5
whiteGraph.arcBackgroundColor = backgroundTrackColor
        
redGraph.endArc = 0.25
redGraph.arcColor = UIColor.redColor()
redGraph.arcWidth = 20.0
redGraph.arcBackgroundColor = backgroundTrackColor

Build and run. Move the sliders around. This looks a little messy.

Screenshot 2015-03-10 15.36.21

The opaque backgrounds of the views is getting in the way of each other. Change the background color for all three views to Clear Color in the storyboard. Run again.

Screenshot 2015-03-10 15.42.16

Now that looks classy! Note we never touched the view code to do this. We built a sufficiently flexible class with properties to control the graph, then added the graphs to the super view.  This is one way of making multiple arcs. You can also do so as part of a single view, but I will leave that up to the reader to implement.

The Whole Code

Here is the code for the lesson. There is a download of a more robust version at the end of another lesson:    where we add pie wedges and some better background track techniques.

ViewController.swift

//
//  ViewController.swift
//  Swift Ring Graph
//
//  Created by Steven Lipton on 3/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var percentLabel: UILabel!
    @IBOutlet weak var circleGraph: CircleGraphView!
    @IBOutlet weak var whiteGraph: CircleGraphView!
    @IBOutlet weak var redGraph: CircleGraphView!
    @IBAction func slider(sender: UISlider) {
        circleGraph.endArc = CGFloat(sender.value)
        percentLabel.text = String(format:" %5.2f %%",sender.value * 100)
    }
    @IBAction func whiteSlider(sender: UISlider) {
        whiteGraph.endArc = CGFloat(sender.value)
    }
    @IBAction func redSlider(sender: UISlider) {
        redGraph.endArc = CGFloat(sender.value)
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let backgroundTrackColor = UIColor(white: 0.15, alpha: 1.0)
        circleGraph.arcBackgroundColor = backgroundTrackColor
        circleGraph.arcWidth = 35.0
        circleGraph.endArc = 0.5

        whiteGraph.arcWidth = 25.0
        whiteGraph.arcColor = UIColor.whiteColor()
        whiteGraph.endArc = 0.5
        whiteGraph.arcBackgroundColor = backgroundTrackColor

        redGraph.endArc = 0.25
        redGraph.arcColor = UIColor.redColor()
        redGraph.arcWidth = 20.0
        redGraph.arcBackgroundColor = backgroundTrackColor

    }

}

CircleGraphView.swift

//
//  CircleGraphView.swift
//  Swift Ring Graph
//
//  Created by Steven Lipton on 3/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class CircleGraphView: UIView {
    var endArc:CGFloat = 0.0{   // in range of 0.0 to 1.0
        didSet{
            setNeedsDisplay()
        }
    }
    var arcWidth:CGFloat = 10.0
    var arcColor = UIColor.yellowColor()
    var arcBackgroundColor = UIColor.blackColor()

    override func drawRect(rect: CGRect) {

        //Important constants for circle
        let fullCircle = 2.0 * CGFloat(M_PI)
        let start:CGFloat = -0.25 * fullCircle
        let end:CGFloat = endArc * fullCircle + start

        //find the centerpoint of the rect
        var centerPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))

        //define the radius by the smallest side of the view
        var radius:CGFloat = 0.0
        if CGRectGetWidth(rect) > CGRectGetHeight(rect){
            radius = (CGRectGetWidth(rect) - arcWidth) / 2.0
        }else{
            radius = (CGRectGetHeight(rect) - arcWidth) / 2.0
        }
        //starting point for all drawing code is getting the context.
        let context = UIGraphicsGetCurrentContext()
        //set colorspace
        let colorspace = CGColorSpaceCreateDeviceRGB()
        //set line attributes
        CGContextSetLineWidth(context, arcWidth)
        CGContextSetLineCap(context, kCGLineCapRound)
        //make the circle background

        CGContextSetStrokeColorWithColor(context, arcBackgroundColor.CGColor)
        CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, fullCircle, 0)
        CGContextStrokePath(context)

        //draw the arc
        CGContextSetStrokeColorWithColor(context, arcColor.CGColor)
        CGContextSetLineWidth(context, arcWidth * 0.8 )
        //CGContextSetLineWidth(context, arcWidth)
        CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, start, end, 0)
        CGContextStrokePath(context)

    }

}

17 Replies to “Swift Swift: Basic Core Graphics for the Ring Graph”

    1. I’m afraid I can’t find a solution the would work in a post either. What I was working on, but had to give up is to make a series of segments. and change the segment color each time. Simple in theory, not so simple in practice. Sorry I wasn’t able to help more.

    1. Good catch! The change doesn’t show in the documentation until pre-release versions in Swift, So I’m assuming this is a 2.0 change, at least officially. I’ll include a note in the text about the change. Thanks.

  1. Thanks for the tutorial, I liked it but I am trying to add something more on this. I am trying to add gradient on each circle. For ex: there is that white circle. I want the same circle but with some gradient on it and my problem is that the cornors are not coming as arc. Could you please help me to achieve that. Thanks a lot in advance.

  2. I enjoyed this tutorial but do have a question… How would you make the the spectrum be larger than 1… or how would you make the portion of the graph that is larger than 1 overlap the original line or arc… I have been messing around with it for quite a while and I am having trouble figuring this out.

    Thanks again for your tutorial

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s