Pie Chart in SwiftUI using Canvas

Apple’s newly introduced Charts Framework in iOS 16 makes it extremely simple to create charts and graphs for apps, but the library still lacks a few standard charts. The pie chart is one example of such a chart. The pie chart is one of the most commonly used charts for data visualization, and we will create one from scratch in this article. Even though a pie chart is not included out of the box in SwiftUI, it is simple to create with some basic components.
Let’s get going. We will start with a data model for the chart.
struct PieModel: Identifiable {
let id = UUID()
var value: Double
var color: Color
var name: String
}
We need data to work with so let’s create sample data
extension PieModel {
static var sample: [PieModel] {
[
.init(value: 10, color: .orange, name: "Orange"),
.init(value: 20, color: .blue, name: "Blue"),
.init(value: 30, color: .indigo, name: "Indigo"),
.init(value: 40, color: .purple, name: "Purple"),
.init(value: 50, color: .cyan, name: "Cyan"),
.init(value: 60, color: .teal, name: "Teal")
]
}
}
We will add a simple animation to the pie chart so let’s create pie view and add two state properties.
- A boolean flag to trigger animation
- An array of pie model to render pie slices
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
Inside the body property for pie view, we will add a VStack and Canvas
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
We need to compute total from all slices so we can plot them inside a circle. For this, we will use reduce function to get total for the pie chart by getting value for each slice and adding it to the running total.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
Let’s move the drawing point to the center of the canvas view. We can do this by computing half of width and height.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
Circle drawing starts horizontally so let’s move it vertical by rotating context by 90 degrees
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
Next, we will create a variable to compute the radius of pie chart.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
It’s time to start drawing arcs to draw the pie chart. We will create and initialize startAngle and iterate over all slices of the pie.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
A circle is 360 degrees so we will have to compute angle for each slice based on its value against the total value.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
// compute angle for each slice
let angle = Angle(degrees: 360 * (slice.value / total))
We will compute the end angle for arc by adding startAngle and the computed angle.
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
// compute angle for each slice
let angle = Angle(degrees: 360 * (slice.value / total))
// compute end angle for slice with start and computed angle
let endAngle = startAngle + angle
We have everything we need to draw arc path.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
// compute angle for each slice
let angle = Angle(degrees: 360 * (slice.value / total))
// compute end angle for slice with start and computed angle
let endAngle = startAngle + angle
// draw arc for each angle
let path = Path { p in
p.move(to: .zero)
p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
p.closeSubpath()
}
Let’s also set the fill and stroke color for each slice. Before looping over the next slice, let’s update start angle to be the end angle for the current slice. This will connect two slices building a full circle eventually.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
// compute angle for each slice
let angle = Angle(degrees: 360 * (slice.value / total))
// compute end angle for slice with start and computed angle
let endAngle = startAngle + angle
// draw arc for each angle
let path = Path { p in
p.move(to: .zero)
p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
p.closeSubpath()
}
// fill current path with slice color
pieContext.fill(path, with: .color(slice.color.opacity(0.6)))
// add stroke line to each slice
pieContext.stroke(path, with: .color(slice.color), lineWidth: 2)
// update start angle with last end angle
startAngle = endAngle
}
We will add rotation and scale animation to the canvas.
struct Pie: View {
@State private var animate = false
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
// compute angle for each slice
let angle = Angle(degrees: 360 * (slice.value / total))
// compute end angle for slice with start and computed angle
let endAngle = startAngle + angle
// draw arc for each angle
let path = Path { p in
p.move(to: .zero)
p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
p.closeSubpath()
}
// fill current path with slice color
pieContext.fill(path, with: .color(slice.color.opacity(0.6)))
// add stroke line to each slice
pieContext.stroke(path, with: .color(slice.color), lineWidth: 2)
// update start angle with last end angle
startAngle = endAngle
}
}
.rotationEffect(animate ? .zero : .degrees(270), anchor: .center)
.scaleEffect(animate ? 1.0 : 0.0, anchor: .center)
.aspectRatio(1, contentMode: .fit)
Let’s use LazyVGrid to create the legend table for the chart and also add a tap gesture recognizer to animate the chart.
struct Pie: View {
@State private var animate = true
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
// compute angle for each slice
let angle = Angle(degrees: 360 * (slice.value / total))
// compute end angle for slice with start and computed angle
let endAngle = startAngle + angle
// draw arc for each angle
let path = Path { p in
p.move(to: .zero)
p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
p.closeSubpath()
}
// fill current path with slice color
pieContext.fill(path, with: .color(slice.color.opacity(0.6)))
// add stroke line to each slice
pieContext.stroke(path, with: .color(slice.color), lineWidth: 2)
// update start angle with last end angle
startAngle = endAngle
}
}
.rotationEffect(animate ? .zero : .degrees(270), anchor: .center)
.scaleEffect(animate ? 1.0 : 0.0, anchor: .center)
.aspectRatio(1, contentMode: .fit)
// legend view
VStack {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) {
ForEach(slices) { item in
HStack {
Circle()
.foregroundStyle(item.color.gradient)
.frame(width: 20, height: 20)
Text(item.name) + Text("(\(item.value.formatted(.number)))")
Spacer()
}
}
}
}
}
.onTapGesture {
withAnimation(.spring(dampingFraction: 0.4)) {
animate.toggle()
}
}
.padding()
}
}
Build and run.

Complete code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
Pie(slices: PieModel.sample)
.navigationTitle("DevTechie.com")
}
}
}
struct PieModel: Identifiable {
let id = UUID()
var value: Double
var color: Color
var name: String
}
extension PieModel {
static var sample: [PieModel] {
[
.init(value: 10, color: .orange, name: "Orange"),
.init(value: 20, color: .blue, name: "Blue"),
.init(value: 30, color: .indigo, name: "Indigo"),
.init(value: 40, color: .purple, name: "Purple"),
.init(value: 50, color: .cyan, name: "Cyan"),
.init(value: 60, color: .teal, name: "Teal")
]
}
}
struct Pie: View {
@State private var animate = true
@State var slices: [PieModel]
var body: some View {
VStack {
Canvas { context, size in
// total area value
let total = slices.reduce(0) { $0 + $1.value }
// move context to the center of the canvas
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
// create copy of context to update
var pieContext = context
// rotate pie chart by 90 degrees
pieContext.rotate(by: .degrees(90))
// radius for the pie chart size
let radius = min(size.width, size.height) * 0.45
// set start angle to zero
var startAngle = Angle.zero
// iterate over all data points to draw each slice
for slice in slices {
// compute angle for each slice
let angle = Angle(degrees: 360 * (slice.value / total))
// compute end angle for slice with start and computed angle
let endAngle = startAngle + angle
// draw arc for each angle
let path = Path { p in
p.move(to: .zero)
p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
p.closeSubpath()
}
// fill current path with slice color
pieContext.fill(path, with: .color(slice.color.opacity(0.6)))
// add stroke line to each slice
pieContext.stroke(path, with: .color(slice.color), lineWidth: 2)
// update start angle with last end angle
startAngle = endAngle
}
}
.rotationEffect(animate ? .zero : .degrees(270), anchor: .center)
.scaleEffect(animate ? 1.0 : 0.0, anchor: .center)
.aspectRatio(1, contentMode: .fit)
// legend view
VStack {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) {
ForEach(slices) { item in
HStack {
Circle()
.foregroundStyle(item.color.gradient)
.frame(width: 20, height: 20)
Text(item.name) + Text("(\(item.value.formatted(.number)))")
Spacer()
}
}
}
}
}
.onTapGesture {
withAnimation(.spring(dampingFraction: 0.4)) {
animate.toggle()
}
}
.padding()
}
}
With that we have reached the end of this article. Thank you once again for reading. Don’t forget to 👏 and follow 😍. Also subscribe our newsletter at https://www.devtechie.com