Pie Chart in SwiftUI

Manuel Kunz
StepUp Development
Published in
7 min readApr 3, 2021

Recently two friends and I were participating in a SwiftUI Jam. Over one weekend we were developing a little application in SwiftUI, which was a lot of fun.

The dashboard of the application shows multiple statistics, one of them presented as a pie chart.

In the following I want to show you the approach we took to make a simple reusable pie chart in SwiftUI.

At first I’m going to show you how to make a pie chart with fixed values to get the hang on how the slices are drawn. Further into the article I will show you our approach on making the pie chart easier to use and responsive in its size.

Drawing a circle

Drawing a circle in SwiftUI could not be easier.

Circle()

That’s it, but this will not take us far, so what we really need is drawing a slice of a pie in every size we want. So first, let’s take a look on how to draw for example a half circle. For this we are going to use paths.

I’m not going to show you how paths in general work, but with paths and arcs you can draw much more complex forms than circles.

Draw a half circle

The following code creates a half circle with a radius of 100 from degree 0 to degree 180.

.move moves the path to the correct starting point. .addArc draws an arc around the given center between the two angles with the given radius.

This is all you need to draw a half circle.

Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: false)
}
.fill(Color.red)

What sometimes can be a little bit tricky, at least it was for me, is the meaning of clockwise.

In the official Apple Documentation you can see an image of a circle to understand which degree is at which position of the circle. Clockwise is defined as ‘The direction in which to draw the arc’. So one, like me, would think of a clock we all know and would expect the code in the example above with clockwise false to draw an half circle with the top half filled red, but it is the complete opposite, you will get the lower half of the circle. It is a little bit confusing and might clash with the way SwiftUI handles coordinate systems as explained in this answer by rob-mayoff.

For the following this will not be relevant because we will just work with what is given to us. I will choose clockwise as false, I’m sure this can also be done with clockwise true, but you will have to adjust the calculation later on.

Draw a Pie Chart with fixed values

But before we get to fancy calculations let’s draw our first pie chart.

ZStack(alignment: .center) {
Path { path in
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: false)
}
.fill(Color.red)
Path { path in
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: Angle(degrees: 180),
endAngle: Angle(degrees: 360),
clockwise: false)
}
.fill(Color.blue)
}

We put the Path from the example in a ZStack to place multiple elements above each other and adjusted the start and end Angle for the second Path, this results in a pie chart with two halfs.

For fixed values this is all you would need, but in most cases the values shown are not fixed so let’s have a look at how we can handle this.

Simplify the UI

Our pie chart will exist of multiple slices, I ended up calling them a PieSlice. In the example above each Path is one PieSlice, so we will wrap this in a struct:

private struct PieSlice : Hashable {
var start: Angle
var end: Angle
var color: Color
}

In the body of the view, we can now iterate (that’s why we need the PieSlice to perform to Hashable) the pie slices, remove the fixed angles and the color.

struct PieChart: View {
private var slices = [
PieSlice(start: Angle(degrees: 0),
end: Angle(degrees: 180),
color: .red),
PieSlice(start: Angle(degrees: 180),
end: Angle(degrees: 360),
color: .blue)
]
var body: some View {
ZStack(alignment: .center) {
ForEach(slices, id: \.self) { slice in
Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: CGPoint(x: 200, y: 200),
radius: 100,
startAngle: slice.start,
endAngle: slice.end,
clockwise: false
)
}
.fill(slice.color)
}
}
}
}
private struct PieSlice : Hashable {
var start: Angle
var end: Angle
var color: Color
}

The pie chart can now easily be extended with more slices. The fixed values of radius and the center point will removed later on, we will now do some calculations to get rid off the fixed angles.

Calculation

When creating some kind of reusable component the saying of ‘Eat your own dog food’ always comes up to my mind. So I want to make my pie charts as easy to use as possible. The minimum of information we have to provide are the values and the color in which a value should be displayed (if the colors can be random you could also come up with some predefined colors or a RandomColorGenerator).

init(_ values: [(Color, Double)]) {
slices = calculateSlices(from: values)
}
// Usage for the example abovePieChart([
(.red, 50),
(.blue, 50)
])

This is where our calculation has to come in. We have the given values that should be displayed and we need to convert these values into our slices. This will be done in the calculateSlices function.

private func calculateSlices(from inputValues: [(color: Color, value: Double)]) -> [PieSlice] {    // 1
let sumOfAllValues = inputValues.reduce(0) { $0 + $1.value }
guard sumOfAllValues > 0 else {
return []
}
let degreeForOneValue = 360.0 / sumOfAllValues // 2
var slices = [PieSlice]()
var currentStartAngle = 0.0
inputValues.forEach { inputValue in
let endAngle = degreeForOneValue * inputValue.value +
currentStartAngle
slices.append(
PieSlice(
start: Angle(degrees: currentStartAngle),
end: Angle(degrees: endAngle),
color: inputValue.color
)
)
currentStartAngle = endAngle
}
return slices
}

This might seem a bit complex at first but let’s break it down.
In the first section two calculations are done.

First we get the sum of all values. We will then devide 360.0 degrees by this sum.
This will give us the degrees which are needed to display a single value.

For example the input values are 20, 30 and 50. The sum of this values are 100.

let degreeForOneValue = 360.0 / 100
// Will be 3.6

In this example we will need 3.6 degrees to display the value 1. For 50 we will need 3,6 * 50 which is 180 this will take half of the circle.

In the second section of the calculation we iterate the input values and map them to pie slices. Additional to the conversion of value to degree we have to handle the start of each angle. Each PieSlice should start at the end angle of the previous PieSlice, this is handled with the currentStartAngle which will be added to each calculated end angle. At the end of each iteration the currentStartAngle will be set to the endAngle of the calculated PieSlice.

This is all we have to do to create a pie chart which is pretty simple to use. The example above with two half circles can now be achieved like this:

PieChart([
(.red, 50),
(.blue, 50),
])
// or thisPieChart([
(.red, 1),
(.blue, 1),
])

We can now create any pie chart. But we are not finished yet, our pie chart is dynamic in values but it is pretty fixed in its size with a hard coded center point and a radius which is fixed to 100.

Responsive size for the Pie Chart

We can get the size of the parent view by wrapping the ZStack into a GeometryReader. Our pie chart will be placed in some view, so it will be some kind of rectangle. This rectangle can have a different height and width in which we want to use as much space without overlapping.

So what do we need? First the center of the rectangle. Second the radius the pie chart can have to fit into the rectangle.

The center is the half of the width for x and the half of the height for y. These values can be accessed on the GeometryReader.

The height and width of the rectangle are most likely not the same value, except a square, we have to use the smaller of both values as radius, the pie chart then has the biggest size possible without overlapping.

GeometryReader { reader in
let halfWidth = (reader.size.width / 2)
let halfHeight = (reader.size.height / 2)
let radius = min(halfWidth, halfHeight)
let center = CGPoint(x: halfWidth, y: halfHeight)
ZStack(alignment: .center) {
ForEach(slices, id: \.self) { slice in
Path { path in
path.move(to: center)
path.addArc(center: center,
radius: radius,
startAngle: slice.start,
endAngle: slice.end,
clockwise: false
)
}
.fill(slice.color)
}
}
}

And that’s it. I’d love to hear any kind of feedback.

The example project can be found on GitHub. Show me what kind of reusable Views have you done with SwiftUI?

I hope you liked the article, the approach we chose and the way the pie chart can be used.

Feel free to contact me Twitter.

Happy coding!

--

--