If 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:
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:
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.
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.
In the resolver, select Update Frames.
Select the Pizza Power Label. In the Equal width to 0% constraint, click Edit.
Change the Multiplier from 1 to 1:3 or 3:1 so you get this on the storyboard:
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:
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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) } }
Leave a Reply