avatarDevTechie

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

15849

Abstract

="hljs-keyword">let</span> radius <span class="hljs-operator">=</span> <span class="hljs-built_in">min</span>(size.width, size.height) <span class="hljs-operator"></span> <span class="hljs-number">0.45</span> <span class="hljs-comment">// set start angle to zero</span> <span class="hljs-keyword">var</span> startAngle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>.zero <span class="hljs-comment">// iterate over all data points to draw each slice</span> <span class="hljs-keyword">for</span> slice <span class="hljs-keyword">in</span> slices { <span class="hljs-comment">// compute angle for each slice</span> <span class="hljs-keyword">let</span> angle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>(degrees: <span class="hljs-number">360</span> <span class="hljs-operator"></span> (slice.value <span class="hljs-operator">/</span> total))</pre></div><p id="150a">We will compute the end angle for arc by adding startAngle and the computed angle.</p><div id="1496"><pre><span class="hljs-keyword">var</span> body: <span class="hljs-keyword">some</span> <span class="hljs-type">View</span> { <span class="hljs-type">VStack</span> { <span class="hljs-type">Canvas</span> { context, size <span class="hljs-keyword">in</span> <span class="hljs-comment">// total area value</span> <span class="hljs-keyword">let</span> total <span class="hljs-operator">=</span> slices.reduce(<span class="hljs-number">0</span>) { <span class="hljs-variable">0</span> <span class="hljs-operator">+</span> <span class="hljs-variable">1</span>.value } <span class="hljs-comment">// move context to the center of the canvas</span> context.translateBy(x: size.width <span class="hljs-operator"></span> <span class="hljs-number">0.5</span>, y: size.height <span class="hljs-operator"></span> <span class="hljs-number">0.5</span>) <span class="hljs-comment">// create copy of context to update</span> <span class="hljs-keyword">var</span> pieContext <span class="hljs-operator">=</span> context <span class="hljs-comment">// rotate pie chart by 90 degrees</span> pieContext.rotate(by: .degrees(<span class="hljs-number">90</span>)) <span class="hljs-comment">// radius for the pie chart size</span> <span class="hljs-keyword">let</span> radius <span class="hljs-operator">=</span> <span class="hljs-built_in">min</span>(size.width, size.height) <span class="hljs-operator"></span> <span class="hljs-number">0.45</span> <span class="hljs-comment">// set start angle to zero</span> <span class="hljs-keyword">var</span> startAngle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>.zero <span class="hljs-comment">// iterate over all data points to draw each slice</span> <span class="hljs-keyword">for</span> slice <span class="hljs-keyword">in</span> slices { <span class="hljs-comment">// compute angle for each slice</span> <span class="hljs-keyword">let</span> angle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>(degrees: <span class="hljs-number">360</span> <span class="hljs-operator"></span> (slice.value <span class="hljs-operator">/</span> total)) <span class="hljs-comment">// compute end angle for slice with start and computed angle</span> <span class="hljs-keyword">let</span> endAngle <span class="hljs-operator">=</span> startAngle <span class="hljs-operator">+</span> angle</pre></div><p id="75fa">We have everything we need to draw arc path.</p><div id="3ed6"><pre><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Pie</span>: <span class="hljs-title class_">View</span> { <span class="hljs-meta">@State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> animate <span class="hljs-operator">=</span> <span class="hljs-literal">false</span> <span class="hljs-meta">@State</span> <span class="hljs-keyword">var</span> slices: [<span class="hljs-type">PieModel</span>]

<span class="hljs-keyword">var</span> body: <span class="hljs-keyword">some</span> <span class="hljs-type">View</span> {
    <span class="hljs-type">VStack</span> {
        <span class="hljs-type">Canvas</span> { context, size <span class="hljs-keyword">in</span>
            <span class="hljs-comment">// total area value</span>
            <span class="hljs-keyword">let</span> total <span class="hljs-operator">=</span> slices.reduce(<span class="hljs-number">0</span>) { <span class="hljs-variable">$0</span> <span class="hljs-operator">+</span> <span class="hljs-variable">$1</span>.value }
            <span class="hljs-comment">// move context to the center of the canvas</span>
            context.translateBy(x: size.width <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>, y: size.height <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>)
            <span class="hljs-comment">// create copy of context to update</span>
            <span class="hljs-keyword">var</span> pieContext <span class="hljs-operator">=</span> context
            <span class="hljs-comment">// rotate pie chart by 90 degrees</span>
            pieContext.rotate(by: .degrees(<span class="hljs-number">90</span>))
            <span class="hljs-comment">// radius for the pie chart size</span>
            <span class="hljs-keyword">let</span> radius <span class="hljs-operator">=</span> <span class="hljs-built_in">min</span>(size.width, size.height) <span class="hljs-operator">*</span> <span class="hljs-number">0.45</span>
            <span class="hljs-comment">// set start angle to zero</span>
            <span class="hljs-keyword">var</span> startAngle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>.zero
            <span class="hljs-comment">// iterate over all data points to draw each slice</span>
            <span class="hljs-keyword">for</span> slice <span class="hljs-keyword">in</span> slices {
                <span class="hljs-comment">// compute angle for each slice</span>
                <span class="hljs-keyword">let</span> angle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>(degrees: <span class="hljs-number">360</span> <span class="hljs-operator">*</span> (slice.value <span class="hljs-operator">/</span> total))
                <span class="hljs-comment">// compute end angle for slice with start and computed angle</span>
                <span class="hljs-keyword">let</span> endAngle <span class="hljs-operator">=</span> startAngle <span class="hljs-operator">+</span> angle
                <span class="hljs-comment">// draw arc for each angle</span>
                <span class="hljs-keyword">let</span> path <span class="hljs-operator">=</span> <span class="hljs-type">Path</span> { p <span class="hljs-keyword">in</span>
                    p.move(to: .zero)
                    p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: <span class="hljs-literal">false</span>)
                    p.closeSubpath()
                }</pre></div><p id="9562">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.</p><div id="528f"><pre>struct Pie: View {
@State <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> animate = <span class="hljs-literal">false</span>
@State <span class="hljs-keyword">var</span> slices: [PieModel]

<span class="hljs-keyword">var</span> body: some View {
    VStack {
        Canvas { context, size in
            <span class="hljs-comment">// total area value</span>
            let total = slices.<span class="hljs-title function_ invoke__">reduce</span>(<span class="hljs-number">0</span>) { $<span class="hljs-number">0</span> + $<span class="hljs-number">1</span>.value }
            <span class="hljs-comment">// move context to the center of the canvas</span>
            context.<span class="hljs-title function_ invoke__">translateBy</span>(<span class="hljs-attr">x</span>: size.width * <span class="hljs-number">0.5</span>, <span class="hljs-attr">y</span>: size.height * <span class="hljs-number">0.5</span>)
            <span class="hljs-comment">// create copy of context to update</span>
            <span class="hljs-keyword">var</span> pieContext = context
            <span class="hljs-comment">// rotate pie chart by 90 degrees</span>
            pieContext.<span class="hljs-title function_ invoke__">rotate</span>(<span class="hljs-attr">by</span>: .<span class="hljs-title function_ invoke__">degrees</span>(<span class="hljs-number">90</span>))
            <span class="hljs-comment">// radius for the pie chart size</span>
            let radius = <span class="hljs-title function_ invoke__">min</span>(size.width, size.height) * <span class="hljs-number">0.45</span>
            <span class="hljs-comment">// set start angle to zero</span>
            <span class="hljs-keyword">var</span> startAngle = Angle.zero
            <span class="hljs-comment">// iterate over all data points to draw each slice</span>
            <span class="hljs-keyword">for</span> slice in slices {
                <span class="hljs-comment">// compute angle for each slice</span>
                let angle = <span class="hljs-title function_ invoke__">Angle</span>(<span class="hljs-attr">degrees</span>: <span class="hljs-number">360</span> * (slice.value / total))
                <span class="hljs-comment">// compute end angle for slice with start and computed angle</span>
                let endAngle = startAngle + angle
                <span class="hljs-comment">// draw arc for each angle</span>
                let path = Path { p in
                    p.<span class="hljs-title function_ invoke__">move</span>(<span class="hljs-attr">to</span>: .zero)
                    p.<span class="hljs-title function_ invoke__">addArc</span>(<span class="hljs-attr">center</span>: .zero, <span class="hljs-attr">radius</span>: radius, <span class="hljs-attr">startAngle</span>: startAngle, <span class="hljs-attr">endAngle</span>: endAngle, <span class="hljs-attr">clockwise</span>: <span class="hljs-literal">false</span>)
                    p.<span class="hljs-title function_ invoke__">closeSubpath</span>()
                }
                <span class="hljs-comment">// fill current path with slice color</span>
                pieContext.<span class="hljs-title function_ invoke__">fill</span>(path, <span class="hljs-attr">with</span>: .<span class="hljs-title function_ invoke__">color</span>(slice.color.<span class="hljs-title function_ invoke__">opacity</span>(<span class="hljs-number">0.6</span>)))
                <span class="hljs-comment">// add stroke line to each slice</span>
                pieContext.<span class="hljs-title function_ invoke__">stroke</span>(path, <span class="hljs-attr">with</span>: .<span class="hljs-title function_ invoke__">color</span>(slice.color), <span class="hljs-attr">lineWidth</span>: <span class="hljs-number">2</span>)
                <span class="hljs-comment">// update start angle with last end angle</span>
                startAngle = endAngle
            }</pre></div><p id="d4c9">We will add rotation and scale animation to the canvas.</p><div id="4252"><pre><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Pie</span>: <span class="hljs-title class_">View</span> {
<span class="hljs-meta">@State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> animate <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>
<span class="hljs-meta">@State</span> <span class="hljs-keyword">var</span> slices: [<span class="hljs-type">PieModel</span>]

<span class="hljs-keyword">var</span> body: <span class="hljs-keyword">some</span> <span class="hljs-type">View</span> {
    <span class="hljs-type">VStack</span> {
        <span class="hljs-type">Canvas</span> { context, size <span class="hljs-keyword">in</span>
            <span class="hljs-comment">// total area value</span>
            <span class="hljs-keyword">let</span> total <span class="hljs-operator">=</span> slices.reduce(<span class="hljs-number">0</span>) { <span class="hljs-variable">$0</span> <span class="hljs-operator">+</span> <span class="hljs-variable">$1</span>.value }
            <span class="hljs-comment">// move context to the center of the canvas</span>
            context.translateBy(x: size.width <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>, y: size.height <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>)
            <span class="hljs-comment">// create copy of context to update</span>
            <span class="hljs-keyword">var</span> pieContext <span class="hljs-operator">=</span> context
            <span class="hljs-comment">// rotate pie chart by 90 degrees</span>
            pieContext.rotate(by: .degrees(<span class="hljs-number">90</span>))
            <span class="hljs-comment">// radius for the pie chart size</span>
            <span class="hljs-keyword">let</span> radius <span class="hljs-operator">=</span> <span class="hljs-built_in">min</span>(size.width, size.height) <span class="hljs-operator">*</span> <span class="hljs-number">0.45</span>
            <span class="hljs-comment">// set start angle to zero</span>
            <span class="hljs-keyword">var</span> startAngle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>.zero
            <span class="hljs-comment">// iterate over all data points to draw each slice</span>
            <span class="hljs-keyword">for</span> slice <span class="hljs-keyword">in</span> slices {
                <span class="hljs-comment">// compute angle for each slice</span>
                <span class="hljs-keyword">let</span> angle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>(degrees: <span class="hljs-number">360</span> <span class="hljs-operator">*</span> (slice.value <span class="hljs-operator">/</span> total))
                <span class="hljs-comment">// compute end angle for slice with start and computed angle</span>
                <span class="hljs-keyword">let</span> endAngle <span class="hljs-operator">=</span> startAngle <span class="hljs-operator">+</span> angle
                <span class="hljs-comment">// draw arc for each angle</span>
                <span class="hljs-keyword">let</span> path <span class="hljs-operator">=</span> <span class="hljs-type">Path</span> { p <span class="hljs-keyword">in</span>
                    p.move(to: .zero)
                    p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: <span class="hljs-literal">false</span>)
                    p.closeSubpath()
                }
                <span class="hljs-comment">// fill current path with slice color</span>
                pieContext.fill(path, with

Options

: .color(slice.color.opacity(<span class="hljs-number">0.6</span>))) <span class="hljs-comment">// add stroke line to each slice</span> pieContext.stroke(path, with: .color(slice.color), lineWidth: <span class="hljs-number">2</span>) <span class="hljs-comment">// update start angle with last end angle</span> startAngle <span class="hljs-operator">=</span> endAngle } } .rotationEffect(animate <span class="hljs-operator">?</span> .zero : .degrees(<span class="hljs-number">270</span>), anchor: .center) .scaleEffect(animate <span class="hljs-operator">?</span> <span class="hljs-number">1.0</span> : <span class="hljs-number">0.0</span>, anchor: .center) .aspectRatio(<span class="hljs-number">1</span>, contentMode: .fit)</pre></div><p id="7076">Let’s use LazyVGrid to create the legend table for the chart and also add a tap gesture recognizer to animate the chart.</p><div id="9f6f"><pre><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Pie</span>: <span class="hljs-title class_">View</span> { <span class="hljs-meta">@State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> animate <span class="hljs-operator">=</span> <span class="hljs-literal">true</span> <span class="hljs-meta">@State</span> <span class="hljs-keyword">var</span> slices: [<span class="hljs-type">PieModel</span>]

<span class="hljs-keyword">var</span> body: <span class="hljs-keyword">some</span> <span class="hljs-type">View</span> {
    <span class="hljs-type">VStack</span> {
        <span class="hljs-type">Canvas</span> { context, size <span class="hljs-keyword">in</span>
            <span class="hljs-comment">// total area value</span>
            <span class="hljs-keyword">let</span> total <span class="hljs-operator">=</span> slices.reduce(<span class="hljs-number">0</span>) { <span class="hljs-variable">$0</span> <span class="hljs-operator">+</span> <span class="hljs-variable">$1</span>.value }
            <span class="hljs-comment">// move context to the center of the canvas</span>
            context.translateBy(x: size.width <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>, y: size.height <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>)
            <span class="hljs-comment">// create copy of context to update</span>
            <span class="hljs-keyword">var</span> pieContext <span class="hljs-operator">=</span> context
            <span class="hljs-comment">// rotate pie chart by 90 degrees</span>
            pieContext.rotate(by: .degrees(<span class="hljs-number">90</span>))
            <span class="hljs-comment">// radius for the pie chart size</span>
            <span class="hljs-keyword">let</span> radius <span class="hljs-operator">=</span> <span class="hljs-built_in">min</span>(size.width, size.height) <span class="hljs-operator">*</span> <span class="hljs-number">0.45</span>
            <span class="hljs-comment">// set start angle to zero</span>
            <span class="hljs-keyword">var</span> startAngle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>.zero
            <span class="hljs-comment">// iterate over all data points to draw each slice</span>
            <span class="hljs-keyword">for</span> slice <span class="hljs-keyword">in</span> slices {
                <span class="hljs-comment">// compute angle for each slice</span>
                <span class="hljs-keyword">let</span> angle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>(degrees: <span class="hljs-number">360</span> <span class="hljs-operator">*</span> (slice.value <span class="hljs-operator">/</span> total))
                <span class="hljs-comment">// compute end angle for slice with start and computed angle</span>
                <span class="hljs-keyword">let</span> endAngle <span class="hljs-operator">=</span> startAngle <span class="hljs-operator">+</span> angle
                <span class="hljs-comment">// draw arc for each angle</span>
                <span class="hljs-keyword">let</span> path <span class="hljs-operator">=</span> <span class="hljs-type">Path</span> { p <span class="hljs-keyword">in</span>
                    p.move(to: .zero)
                    p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: <span class="hljs-literal">false</span>)
                    p.closeSubpath()
                }
                <span class="hljs-comment">// fill current path with slice color</span>
                pieContext.fill(path, with: .color(slice.color.opacity(<span class="hljs-number">0.6</span>)))
                <span class="hljs-comment">// add stroke line to each slice</span>
                pieContext.stroke(path, with: .color(slice.color), lineWidth: <span class="hljs-number">2</span>)
                <span class="hljs-comment">// update start angle with last end angle</span>
                startAngle <span class="hljs-operator">=</span> endAngle
            }
        }
        .rotationEffect(animate <span class="hljs-operator">?</span> .zero : .degrees(<span class="hljs-number">270</span>), anchor: .center)
        .scaleEffect(animate <span class="hljs-operator">?</span> <span class="hljs-number">1.0</span> : <span class="hljs-number">0.0</span>, anchor: .center)
        .aspectRatio(<span class="hljs-number">1</span>, contentMode: .fit)
        <span class="hljs-comment">// legend view</span>
        <span class="hljs-type">VStack</span> {
            <span class="hljs-type">LazyVGrid</span>(columns: [<span class="hljs-type">GridItem</span>(.adaptive(minimum: <span class="hljs-number">120</span>))]) {
                <span class="hljs-type">ForEach</span>(slices) { item <span class="hljs-keyword">in</span> 
                    <span class="hljs-type">HStack</span> {
                        <span class="hljs-type">Circle</span>()
                            .foregroundStyle(item.color.gradient)
                            .frame(width: <span class="hljs-number">20</span>, height: <span class="hljs-number">20</span>)
                        <span class="hljs-type">Text</span>(item.name) <span class="hljs-operator">+</span> <span class="hljs-type">Text</span>(<span class="hljs-string">"(<span class="hljs-subst">\(item.value.formatted(.number))</span>)"</span>)
                        <span class="hljs-type">Spacer</span>()
                    }
                }
            }
        }
    }
    .onTapGesture {
        withAnimation(.spring(dampingFraction: <span class="hljs-number">0.4</span>)) { 
            animate.toggle()
        }
    }
    .padding()
}

}</pre></div><p id="0b71">Build and run.</p><figure id="d29a"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*uvrn0qilOoIJFdgt3AxgVw.gif"><figcaption></figcaption></figure><p id="5cd8">Complete code:</p><div id="0fb0"><pre><span class="hljs-keyword">import</span> SwiftUI

<span class="hljs-keyword">struct</span> <span class="hljs-title class_">ContentView</span>: <span class="hljs-title class_">View</span> { <span class="hljs-keyword">var</span> body: <span class="hljs-keyword">some</span> <span class="hljs-type">View</span> { <span class="hljs-type">NavigationStack</span> { <span class="hljs-type">Pie</span>(slices: <span class="hljs-type">PieModel</span>.sample) .navigationTitle(<span class="hljs-string">"DevTechie.com"</span>) } } }

<span class="hljs-keyword">struct</span> <span class="hljs-title class_">PieModel</span>: <span class="hljs-title class_">Identifiable</span> { <span class="hljs-keyword">let</span> id <span class="hljs-operator">=</span> <span class="hljs-type">UUID</span>() <span class="hljs-keyword">var</span> value: <span class="hljs-type">Double</span> <span class="hljs-keyword">var</span> color: <span class="hljs-type">Color</span> <span class="hljs-keyword">var</span> name: <span class="hljs-type">String</span> }

<span class="hljs-keyword">extension</span> <span class="hljs-title class_">PieModel</span> { <span class="hljs-keyword">static</span> <span class="hljs-keyword">var</span> sample: [<span class="hljs-type">PieModel</span>] { [ .<span class="hljs-keyword">init</span>(value: <span class="hljs-number">10</span>, color: .orange, name: <span class="hljs-string">"Orange"</span>), .<span class="hljs-keyword">init</span>(value: <span class="hljs-number">20</span>, color: .blue, name: <span class="hljs-string">"Blue"</span>), .<span class="hljs-keyword">init</span>(value: <span class="hljs-number">30</span>, color: .indigo, name: <span class="hljs-string">"Indigo"</span>), .<span class="hljs-keyword">init</span>(value: <span class="hljs-number">40</span>, color: .purple, name: <span class="hljs-string">"Purple"</span>), .<span class="hljs-keyword">init</span>(value: <span class="hljs-number">50</span>, color: .cyan, name: <span class="hljs-string">"Cyan"</span>), .<span class="hljs-keyword">init</span>(value: <span class="hljs-number">60</span>, color: .teal, name: <span class="hljs-string">"Teal"</span>) ] } }

<span class="hljs-keyword">struct</span> <span class="hljs-title class_">Pie</span>: <span class="hljs-title class_">View</span> { <span class="hljs-meta">@State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> animate <span class="hljs-operator">=</span> <span class="hljs-literal">true</span> <span class="hljs-meta">@State</span> <span class="hljs-keyword">var</span> slices: [<span class="hljs-type">PieModel</span>]

<span class="hljs-keyword">var</span> body: <span class="hljs-keyword">some</span> <span class="hljs-type">View</span> {
    <span class="hljs-type">VStack</span> {
        <span class="hljs-type">Canvas</span> { context, size <span class="hljs-keyword">in</span>
            <span class="hljs-comment">// total area value</span>
            <span class="hljs-keyword">let</span> total <span class="hljs-operator">=</span> slices.reduce(<span class="hljs-number">0</span>) { <span class="hljs-variable">$0</span> <span class="hljs-operator">+</span> <span class="hljs-variable">$1</span>.value }
            <span class="hljs-comment">// move context to the center of the canvas</span>
            context.translateBy(x: size.width <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>, y: size.height <span class="hljs-operator">*</span> <span class="hljs-number">0.5</span>)
            <span class="hljs-comment">// create copy of context to update</span>
            <span class="hljs-keyword">var</span> pieContext <span class="hljs-operator">=</span> context
            <span class="hljs-comment">// rotate pie chart by 90 degrees</span>
            pieContext.rotate(by: .degrees(<span class="hljs-number">90</span>))
            <span class="hljs-comment">// radius for the pie chart size</span>
            <span class="hljs-keyword">let</span> radius <span class="hljs-operator">=</span> <span class="hljs-built_in">min</span>(size.width, size.height) <span class="hljs-operator">*</span> <span class="hljs-number">0.45</span>
            <span class="hljs-comment">// set start angle to zero</span>
            <span class="hljs-keyword">var</span> startAngle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>.zero
            <span class="hljs-comment">// iterate over all data points to draw each slice</span>
            <span class="hljs-keyword">for</span> slice <span class="hljs-keyword">in</span> slices {
                <span class="hljs-comment">// compute angle for each slice</span>
                <span class="hljs-keyword">let</span> angle <span class="hljs-operator">=</span> <span class="hljs-type">Angle</span>(degrees: <span class="hljs-number">360</span> <span class="hljs-operator">*</span> (slice.value <span class="hljs-operator">/</span> total))
                <span class="hljs-comment">// compute end angle for slice with start and computed angle</span>
                <span class="hljs-keyword">let</span> endAngle <span class="hljs-operator">=</span> startAngle <span class="hljs-operator">+</span> angle
                <span class="hljs-comment">// draw arc for each angle</span>
                <span class="hljs-keyword">let</span> path <span class="hljs-operator">=</span> <span class="hljs-type">Path</span> { p <span class="hljs-keyword">in</span>
                    p.move(to: .zero)
                    p.addArc(center: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: <span class="hljs-literal">false</span>)
                    p.closeSubpath()
                }
                <span class="hljs-comment">// fill current path with slice color</span>
                pieContext.fill(path, with: .color(slice.color.opacity(<span class="hljs-number">0.6</span>)))
                <span class="hljs-comment">// add stroke line to each slice</span>
                pieContext.stroke(path, with: .color(slice.color), lineWidth: <span class="hljs-number">2</span>)
                <span class="hljs-comment">// update start angle with last end angle</span>
                startAngle <span class="hljs-operator">=</span> endAngle
            }
        }
        .rotationEffect(animate <span class="hljs-operator">?</span> .zero : .degrees(<span class="hljs-number">270</span>), anchor: .center)
        .scaleEffect(animate <span class="hljs-operator">?</span> <span class="hljs-number">1.0</span> : <span class="hljs-number">0.0</span>, anchor: .center)
        .aspectRatio(<span class="hljs-number">1</span>, contentMode: .fit)
        <span class="hljs-comment">// legend view</span>
        <span class="hljs-type">VStack</span> {
            <span class="hljs-type">LazyVGrid</span>(columns: [<span class="hljs-type">GridItem</span>(.adaptive(minimum: <span class="hljs-number">120</span>))]) {
                <span class="hljs-type">ForEach</span>(slices) { item <span class="hljs-keyword">in</span> 
                    <span class="hljs-type">HStack</span> {
                        <span class="hljs-type">Circle</span>()
                            .foregroundStyle(item.color.gradient)
                            .frame(width: <span class="hljs-number">20</span>, height: <span class="hljs-number">20</span>)
                        <span class="hljs-type">Text</span>(item.name) <span class="hljs-operator">+</span> <span class="hljs-type">Text</span>(<span class="hljs-string">"(<span class="hljs-subst">\(item.value.formatted(.number))</span>)"</span>)
                        <span class="hljs-type">Spacer</span>()
                    }
                }
            }
        }
    }
    .onTapGesture {
        withAnimation(.spring(dampingFraction: <span class="hljs-number">0.4</span>)) { 
            animate.toggle()
        }
    }
    .padding()
}

}</pre></div><p id="eab3" type="7">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</p></article></body>

Pie Chart in SwiftUI using Canvas

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

Swiftui
Swiftui 4
Charts
iOS
iOS App Development
Recommended from ReadMedium