{"id":709,"date":"2023-05-02T01:48:47","date_gmt":"2023-05-01T23:48:47","guid":{"rendered":"https:\/\/mic.st\/blog\/?p=709"},"modified":"2024-02-10T20:50:09","modified_gmt":"2024-02-10T19:50:09","slug":"draw-line-graphs-in-swiftui","status":"publish","type":"post","link":"https:\/\/mic.st\/blog\/draw-line-graphs-in-swiftui\/","title":{"rendered":"Draw line graphs and charts in SwiftUI"},"content":{"rendered":"\n<p>Graphs and charts are an essential tool for visualizing data, and with SwiftUI, it&#8217;s easy to create custom and interactive graphs (<em>Spoiler:<\/em> we won&#8217;t add any interactivity during this arcticle). You can use them in iOS, iPadOS, and macOS apps. In this article, we&#8217;ll explore how to draw a line graph in SwiftUI. In a follow up article I will show you how to draw bar graphs in SwiftUI.<\/p>\n\n\n\n<p>See my earlier article if you want to draw graphs in Swift: <a href=\"https:\/\/mic.st\/blog\/draw-graphs-easily-with-carekits-ockcartesianchartview\/\">https:\/\/mic.st\/blog\/draw-graphs-easily-with-carekits-ockcartesianchartview\/<\/a><\/p>\n\n\n\n<p>Let&#8217;s start with a really quick recap as you might still be pretty new to SwiftUI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Update: Native API for iOS 16.0+<\/h2>\n\n\n\n<p>The following article is about building a view for showing graphs on your own.<\/p>\n\n\n\n<p>There is also Apple&#8217;s own native framework for drawing graphs in SwiftUI that you can use starting iOS 16.0+.<\/p>\n\n\n\n<p><strong>If possible, you should use this native API<\/strong>, see: <a href=\"https:\/\/developer.apple.com\/documentation\/charts\" target=\"_blank\" rel=\"noopener\">https:\/\/developer.apple.com\/documentation\/charts<\/a><\/p>\n\n\n\n<p class=\"has-text-align-center\"><em>Thanks @Jem for leaving a comment about that!<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">SwiftUI project Setup<\/h2>\n\n\n\n<p>Let&#8217;s take a quick moment to recap on SwiftUI. As you may know, SwiftUI is a modern UI framework that uses a declarative syntax to create user interfaces. For starters, it has way less boilerplate compared to defining UIs or views in general using Swift alone. However, the syntax can look a little strange if you are coming from Swift.<\/p>\n\n\n\n<p>To create a new SwiftUI project, launch Xcode and select &#8220;File&#8221; &gt; &#8220;New&#8221; &gt; &#8220;Project.&#8221; In the &#8220;Create a new Xcode project&#8221; window, select &#8220;App&#8221; and click &#8220;Next.&#8221; Then, choose &#8220;SwiftUI&#8221; as the user interface and give your project a name. Finally, select the directory where you want to save your project and click &#8220;Create.&#8221;<\/p>\n\n\n\n<p>Alright, now we are ready to go! You should see a new Xcode project where you land in a file called &#8220;ContentView.swift&#8221;. On the right side there should be your preview of this rendered <code>ContentView<\/code>.<\/p>\n\n\n\n<p>The classic &#8220;Hello, World!&#8221; content looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">ContentView<\/span>: <span class=\"hljs-title\">View<\/span> <\/span>{\n    <span class=\"hljs-keyword\">var<\/span> body: some <span class=\"hljs-type\">View<\/span> {\n        <span class=\"hljs-type\">VStack<\/span> {\n            <span class=\"hljs-type\">Image<\/span>(systemName: <span class=\"hljs-string\">\"globe\"<\/span>)\n                .imageScale(.large)\n                .foregroundColor(.accentColor)\n            <span class=\"hljs-type\">Text<\/span>(<span class=\"hljs-string\">\"Hello, world!\"<\/span>)\n        }\n        .padding()\n    }\n}\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">ContentView_Previews<\/span>: <span class=\"hljs-title\">PreviewProvider<\/span> <\/span>{\n    <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">var<\/span> previews: some <span class=\"hljs-type\">View<\/span> {\n        <span class=\"hljs-type\">ContentView<\/span>()\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If you never did any SwiftUI before, I can highly recommend to just play a little bit around with this piece of code. Just add any view modifiers and see what happens.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Draw a Line Graph in SwiftUI<\/h2>\n\n\n\n<p>Now that we have our little project set up, let&#8217;s start by draw the line graph itself. As you probably know, a line graph is a simple type of graph that displays data as a series of points connected by straight lines. We could later add some rounding \/ interpolating to make it look &#8220;nicer&#8221; but let&#8217;s start easy first.<\/p>\n\n\n\n<p>Our goal is to pass some data that represents X and Y coordinates to some custom <code>View<\/code> and this <code>View<\/code> should render our data. To make things easier, the view should scale itself accordingly to the minimum and maximum values. The origin of the graph should be at the bottom-left corner.<\/p>\n\n\n\n<p>E.g. the data might look like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-keyword\">let<\/span> data:&#91;(<span class=\"hljs-type\">Double<\/span>, <span class=\"hljs-type\">Double<\/span>)] = &#91;(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>), (<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">5<\/span>), (<span class=\"hljs-number\">2<\/span>, <span class=\"hljs-number\">3<\/span>), (<span class=\"hljs-number\">3<\/span>, <span class=\"hljs-number\">8<\/span>), (<span class=\"hljs-number\">4<\/span>, <span class=\"hljs-number\">4<\/span>), (<span class=\"hljs-number\">5<\/span>, <span class=\"hljs-number\">6<\/span>), (<span class=\"hljs-number\">6<\/span>, <span class=\"hljs-number\">1<\/span>)]<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This data set represents seven points with the x-values ranging from 0 to 6 and the y-values ranging from 0 to 8.<\/p>\n\n\n\n<p>In the following sections I will start with the code followed by a small explanation what happens. The great thing about SwiftUI is that you can quickly play around with it. That means whenever you need to re-draw your line graph in SwiftUI you do not need to re-build. Xcode does instantly render your view in the preview.<\/p>\n\n\n\n<p>Eventually we want to have a custom <code>View<\/code> that we can call like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-keyword\">import<\/span> SwiftUI\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">ContentView<\/span>: <span class=\"hljs-title\">View<\/span> <\/span>{\n  <span class=\"hljs-keyword\">let<\/span> data:&#91;(<span class=\"hljs-type\">Double<\/span>, <span class=\"hljs-type\">Double<\/span>)] = &#91;(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>), (<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">5<\/span>), (<span class=\"hljs-number\">2<\/span>, <span class=\"hljs-number\">3<\/span>), (<span class=\"hljs-number\">3<\/span>, <span class=\"hljs-number\">8<\/span>), (<span class=\"hljs-number\">4<\/span>, <span class=\"hljs-number\">4<\/span>), (<span class=\"hljs-number\">5<\/span>, <span class=\"hljs-number\">6<\/span>), (<span class=\"hljs-number\">6<\/span>, <span class=\"hljs-number\">1<\/span>)]\n\n  <span class=\"hljs-keyword\">var<\/span> body: some <span class=\"hljs-type\">View<\/span> {\n    <span class=\"hljs-type\">LineGraphView<\/span>(data: data)\n  }\n}\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">ContentView_Previews<\/span>: <span class=\"hljs-title\">PreviewProvider<\/span> <\/span>{\n  <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">var<\/span> previews: some <span class=\"hljs-type\">View<\/span> {\n    <span class=\"hljs-type\">ContentView<\/span>()\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Drawing the data<\/h2>\n\n\n\n<p>Let&#8217;s start with the code for drawing the line itself.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">LineGraphView<\/span>: <span class=\"hljs-title\">View<\/span> <\/span>{\n  <span class=\"hljs-keyword\">let<\/span> data: &#91;(<span class=\"hljs-type\">Double<\/span>, <span class=\"hljs-type\">Double<\/span>)]\n  <span class=\"hljs-keyword\">var<\/span> body: some <span class=\"hljs-type\">View<\/span> {\n    <span class=\"hljs-type\">GeometryReader<\/span> { geometry <span class=\"hljs-keyword\">in<\/span>\n      <span class=\"hljs-type\">Path<\/span> { path <span class=\"hljs-keyword\">in<\/span>\n        <span class=\"hljs-keyword\">let<\/span> xScale = geometry.size.width \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>)\n        <span class=\"hljs-keyword\">let<\/span> yScale = geometry.size.height \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }?.<span class=\"hljs-number\">1<\/span> ?? <span class=\"hljs-number\">1<\/span>)\n        path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: <span class=\"hljs-number\">0<\/span>, y: geometry.size.height - <span class=\"hljs-type\">CGFloat<\/span>(data&#91;<span class=\"hljs-number\">0<\/span>].<span class=\"hljs-number\">1<\/span>) * yScale))\n        <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-number\">1<\/span>..&lt;data.<span class=\"hljs-built_in\">count<\/span> {\n          path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: <span class=\"hljs-type\">CGFloat<\/span>(i) * xScale, y: geometry.size.height - <span class=\"hljs-type\">CGFloat<\/span>(data&#91;i].<span class=\"hljs-number\">1<\/span>) * yScale))\n        }\n      }\n      .stroke(<span class=\"hljs-type\">Color<\/span>.blue, lineWidth: <span class=\"hljs-number\">2<\/span>)\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Alright, let&#8217;s break down this code snippet! We start with a <code>GeometryReader<\/code>, which gives us access to the size of the view it&#8217;s contained in. <\/p>\n\n\n\n<p>If you want to learn a little bit more about `GeometryReader`<a href=\"https:\/\/www.hackingwithswift.com\/books\/ios-swiftui\/understanding-frames-and-coordinates-inside-geometryreader\" target=\"_blank\" rel=\"noopener\">https:\/\/www.hackingwithswift.com\/books\/ios-swiftui\/understanding-frames-and-coordinates-inside-geometryreader<\/a> by Paul Hudson.<\/p>\n\n\n\n<p>Next, we create a <code>Path<\/code> and start drawing the line using <code>move(to:)<\/code> to move the drawing cursor to the first point of our data. Then, we loop through the data and draw a line to each subsequent point using <code>addLine(to:)<\/code>.<\/p>\n\n\n\n<p>We calculate the scaling factors for the x and y axes by dividing the view size by the number of data points and the maximum data value, respectively. Finally, we stroke the path with a blue color and a line width of 2 points. And voila, we have ourselves a beautiful line graph!<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"469\" height=\"320\" src=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-1.png\" alt=\"Line graph for seven data points drawn by SwiftUI.\" class=\"wp-image-711\" srcset=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-1.png 469w, https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-1-300x205.png 300w\" sizes=\"auto, (max-width: 469px) 100vw, 469px\" \/><figcaption class=\"wp-element-caption\">Line graph for seven data points drawn by SwiftUI.<\/figcaption><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\">Axis and ticks<\/h2>\n\n\n\n<p>However, what is not so beautiful are the missing axis and ticks on the axis to indicate the data point values. To draw the line graph in SwiftUI was rather easy. Adding ticks involves some more thinking for making it perfect. For simplicity we just want to have a tick for every full value. In the conclusion section there is an example showing that this does not always works really great.<\/p>\n\n\n\n<p>Add the following code into you <code>GeometryReader<\/code> block for adding axis and ticks.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-comment\">\/\/ x-axis<\/span>\n<span class=\"hljs-type\">Path<\/span> { path <span class=\"hljs-keyword\">in<\/span>\n  <span class=\"hljs-keyword\">let<\/span> xScale = geometry.size.width \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>)\n  <span class=\"hljs-keyword\">let<\/span> y = geometry.size.height\n  path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: <span class=\"hljs-number\">0<\/span>, y: y))\n  path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: geometry.size.width, y: y))\n\n  <span class=\"hljs-keyword\">let<\/span> tickSpacing = <span class=\"hljs-number\">1<\/span>\n  <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-number\">0<\/span>..&lt;(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>) {\n    <span class=\"hljs-keyword\">let<\/span> tickX = <span class=\"hljs-type\">CGFloat<\/span>(i * tickSpacing) * xScale\n    <span class=\"hljs-keyword\">guard<\/span> tickX &lt; geometry.size.width <span class=\"hljs-keyword\">else<\/span> { <span class=\"hljs-keyword\">break<\/span> }\n    path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: tickX, y: y - <span class=\"hljs-number\">5<\/span>))\n    path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: tickX, y: y + <span class=\"hljs-number\">5<\/span>))\n  }\n}\n.stroke(<span class=\"hljs-type\">Color<\/span>.red, lineWidth: <span class=\"hljs-number\">5<\/span>)\n\n<span class=\"hljs-comment\">\/\/ y-axis<\/span>\n<span class=\"hljs-type\">Path<\/span> { path <span class=\"hljs-keyword\">in<\/span>\n  <span class=\"hljs-keyword\">let<\/span> x: <span class=\"hljs-type\">CGFloat<\/span> = <span class=\"hljs-number\">0<\/span>\n  <span class=\"hljs-keyword\">let<\/span> yScale: <span class=\"hljs-type\">CGFloat<\/span> = geometry.size.height \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }?.<span class=\"hljs-number\">1<\/span> ?? <span class=\"hljs-number\">1<\/span>)\n  path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: x, y: <span class=\"hljs-number\">0<\/span>))\n  path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: x, y: geometry.size.height))\n\n  <span class=\"hljs-keyword\">let<\/span> tickSpacing: <span class=\"hljs-type\">CGFloat<\/span> = <span class=\"hljs-number\">1<\/span>\n  <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-number\">0<\/span>..&lt;<span class=\"hljs-type\">Int<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }!.<span class=\"hljs-number\">1<\/span>) {\n    <span class=\"hljs-keyword\">let<\/span> tickY = <span class=\"hljs-type\">CGFloat<\/span>(i) * tickSpacing * yScale\n    path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: x - <span class=\"hljs-number\">5<\/span>, y: geometry.size.height - tickY))\n    path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: x + <span class=\"hljs-number\">5<\/span>, y: geometry.size.height - tickY))\n  }\n}\n.stroke(<span class=\"hljs-type\">Color<\/span>.red, lineWidth: <span class=\"hljs-number\">5<\/span>)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>OK, what happens here?<\/p>\n\n\n\n<p>The first block of code creates the x-axis. It starts by defining a <code>Path<\/code> and then setting the starting point at the bottom-left corner of the chart. The x-axis is drawn using the <code>addLine<\/code> method, which creates a line from the starting point to the right-most point of the chart. The <code>tickSpacing<\/code> variable is used to determine the space between each tick on the x-axis. The loop iterates over the data set. Inside of it we draws tick marks at the specified intervals along the X-axis.<\/p>\n\n\n\n<p>The second block of code creates the y-axis. It looks quite similar to the first one. However, for calculating the <code>yScale<\/code> we need to find the maximum Y value of the data set.<\/p>\n\n\n\n<p>Finally, both the x and y axis are drawn using the <code>stroke<\/code> method. This uses the specified color and line width for drawing the path.<\/p>\n\n\n\n<p>For our simple example, we chose a fixed <code>tickSpacing<\/code>  value of 1 so that you can easily read our small number of data points. You might need to change this logic if you want to render larger data sets.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"470\" height=\"320\" src=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-2.png\" alt=\"Line graph drawn in SwiftUI showing seven data points and X and Y axis with ticks for every integer.\" class=\"wp-image-712\" srcset=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-2.png 470w, https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-2-300x204.png 300w\" sizes=\"auto, (max-width: 470px) 100vw, 470px\" \/><figcaption class=\"wp-element-caption\">Our line graph with added axis<\/figcaption><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\">Add some labels<\/h2>\n\n\n\n<p>Still our graph looks a little bit empty.You do not really get an idea of the dimensions. So let&#8217;s add some labels on our ticks. For simplicity, we just add on every tick a label of the tick&#8217;s value. Depending on your expected data you might change the logic here a little bit as well.<\/p>\n\n\n\n<p>You can add the following code also inside of your <code><code>GeometryReader<\/code><\/code> block.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-comment\">\/\/ add x-axis tick labels<\/span>\n<span class=\"hljs-type\">ForEach<\/span>(<span class=\"hljs-number\">1<\/span>..&lt;(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>), id: \\.<span class=\"hljs-keyword\">self<\/span>) { i <span class=\"hljs-keyword\">in<\/span>\n  <span class=\"hljs-keyword\">let<\/span> tickLabel = <span class=\"hljs-type\">Text<\/span>(<span class=\"hljs-string\">\"\\(i)\"<\/span>)\n    .frame(width: <span class=\"hljs-number\">16<\/span>, height: <span class=\"hljs-number\">16<\/span>, alignment: .center)\n  <span class=\"hljs-keyword\">let<\/span> tickX = <span class=\"hljs-type\">CGFloat<\/span>(i) * geometry.size.width \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>)\n  tickLabel\n    .position(x: tickX, y: geometry.size.height - <span class=\"hljs-number\">16<\/span>)\n}\n\n<span class=\"hljs-comment\">\/\/ add y-axis tick labels<\/span>\n<span class=\"hljs-type\">ForEach<\/span>(<span class=\"hljs-number\">1<\/span>..&lt;<span class=\"hljs-type\">Int<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }!.<span class=\"hljs-number\">1<\/span>), id: \\.<span class=\"hljs-keyword\">self<\/span>) { i <span class=\"hljs-keyword\">in<\/span>\n  <span class=\"hljs-keyword\">let<\/span> tickLabel = <span class=\"hljs-type\">Text<\/span>(<span class=\"hljs-string\">\"\\(i)\"<\/span>)\n    .frame(width: <span class=\"hljs-number\">16<\/span>, height: <span class=\"hljs-number\">16<\/span>, alignment: .center)\n  <span class=\"hljs-keyword\">let<\/span> tickY = <span class=\"hljs-type\">CGFloat<\/span>(i) * geometry.size.height \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }!.<span class=\"hljs-number\">1<\/span>)\n  tickLabel\n    .position(x: <span class=\"hljs-number\">16<\/span> - <span class=\"hljs-number\">5<\/span>, y: geometry.size.height - tickY)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We basically loop over the same ranges we had before. I did set the label&#8217;s frame to a fixed size so that their position is easier to calculate.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"471\" height=\"324\" src=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-3.png\" alt=\"Line graph showing six data points, X and Y axis with ticks on each integer and labels on each tick indicating the integer value.\" class=\"wp-image-714\" srcset=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-3.png 471w, https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-3-300x206.png 300w\" sizes=\"auto, (max-width: 471px) 100vw, 471px\" \/><figcaption class=\"wp-element-caption\">Our final simple line graph with ticks and labels.<\/figcaption><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>We did just draw a simple line graph in SwiftUI by using a relatively simple custom view. This handles scaling and adding of axis, ticks and labels completely on it&#8217;s own. If you add more data than the example of just seven values it will automatically scale itself.<\/p>\n\n\n\n<p>From this solution you can easily iterate now. E.g. add support for larger datasets, add animations, more labels, legends, lines, animations, etc.<\/p>\n\n\n\n<p>First thing you will quickly see, is that the given first draft will not look that great for large data sets.<\/p>\n\n\n\n<p>For example given this data:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">ContentView<\/span>: <span class=\"hljs-title\">View<\/span> <\/span>{\n  <span class=\"hljs-keyword\">let<\/span> data:&#91;(<span class=\"hljs-type\">Double<\/span>, <span class=\"hljs-type\">Double<\/span>)] = &#91;(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>), (<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">5<\/span>), (<span class=\"hljs-number\">2<\/span>, <span class=\"hljs-number\">3<\/span>), (<span class=\"hljs-number\">3<\/span>, <span class=\"hljs-number\">30<\/span>), (<span class=\"hljs-number\">4<\/span>, <span class=\"hljs-number\">4<\/span>), (<span class=\"hljs-number\">5<\/span>, <span class=\"hljs-number\">6<\/span>), (<span class=\"hljs-number\">6<\/span>, <span class=\"hljs-number\">1<\/span>), (<span class=\"hljs-number\">7<\/span>, <span class=\"hljs-number\">4<\/span>), (<span class=\"hljs-number\">8<\/span>, <span class=\"hljs-number\">6<\/span>), (<span class=\"hljs-number\">9<\/span>, <span class=\"hljs-number\">1<\/span>)]\n\n  <span class=\"hljs-keyword\">var<\/span> body: some <span class=\"hljs-type\">View<\/span> {\n    <span class=\"hljs-type\">LineGraphView<\/span>(data: data)\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>You get the following output:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"469\" height=\"324\" src=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-4.png\" alt=\"\" class=\"wp-image-715\" srcset=\"https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-4.png 469w, https:\/\/mic.st\/blog\/wp-content\/uploads\/2023\/05\/image-4-300x207.png 300w\" sizes=\"auto, (max-width: 469px) 100vw, 469px\" \/><\/figure>\n<\/div>\n\n\n<p>As you can see, what I mentioned earlier comes into effect here: Because we did just add ticks and labels for every value, the Y axis looks a little bit crowded. You can fix this by adding an additional parameter (e.g. for removing every n-th tick) or you change the logic so that it automatically hides some based on your needs (e.g. based on the view&#8217;s available size).<\/p>\n\n\n\n<p>However, I will leave that up to you now \ud83d\ude42 Happy experimenting!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Full Code example<\/h2>\n\n\n\n<p>ContentView.swift<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-comment\">\/\/<\/span>\n<span class=\"hljs-comment\">\/\/  ContentView.swift<\/span>\n<span class=\"hljs-comment\">\/\/  TestGraph<\/span>\n<span class=\"hljs-comment\">\/\/<\/span>\n<span class=\"hljs-comment\">\/\/  Created by Michael Steudter on 01.05.23.<\/span>\n<span class=\"hljs-comment\">\/\/<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> SwiftUI\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">ContentView<\/span>: <span class=\"hljs-title\">View<\/span> <\/span>{\n  <span class=\"hljs-keyword\">let<\/span> data:&#91;(<span class=\"hljs-type\">Double<\/span>, <span class=\"hljs-type\">Double<\/span>)] = &#91;(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>), (<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">5<\/span>), (<span class=\"hljs-number\">2<\/span>, <span class=\"hljs-number\">3<\/span>), (<span class=\"hljs-number\">3<\/span>, <span class=\"hljs-number\">30<\/span>), (<span class=\"hljs-number\">4<\/span>, <span class=\"hljs-number\">4<\/span>), (<span class=\"hljs-number\">5<\/span>, <span class=\"hljs-number\">6<\/span>), (<span class=\"hljs-number\">6<\/span>, <span class=\"hljs-number\">1<\/span>), (<span class=\"hljs-number\">7<\/span>, <span class=\"hljs-number\">4<\/span>), (<span class=\"hljs-number\">8<\/span>, <span class=\"hljs-number\">6<\/span>), (<span class=\"hljs-number\">9<\/span>, <span class=\"hljs-number\">1<\/span>)]\n\t\n  <span class=\"hljs-keyword\">var<\/span> body: some <span class=\"hljs-type\">View<\/span> {\n    <span class=\"hljs-type\">LineGraphView<\/span>(data: data)\n  }\n}\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">ContentView_Previews<\/span>: <span class=\"hljs-title\">PreviewProvider<\/span> <\/span>{\n  <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">var<\/span> previews: some <span class=\"hljs-type\">View<\/span> {\n    <span class=\"hljs-type\">ContentView<\/span>()\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>LineGraphView.swift<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"Swift\" data-shcb-language-slug=\"swift\"><span><code class=\"hljs language-swift\"><span class=\"hljs-comment\">\/\/<\/span>\n<span class=\"hljs-comment\">\/\/  LineGraphView.swift<\/span>\n<span class=\"hljs-comment\">\/\/  TestGraph<\/span>\n<span class=\"hljs-comment\">\/\/<\/span>\n<span class=\"hljs-comment\">\/\/  Created by Michael Steudter on 01.05.23.<\/span>\n<span class=\"hljs-comment\">\/\/<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> SwiftUI\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">struct<\/span> <span class=\"hljs-title\">LineGraphView<\/span>: <span class=\"hljs-title\">View<\/span> <\/span>{\n  <span class=\"hljs-keyword\">let<\/span> data: &#91;(<span class=\"hljs-type\">Double<\/span>, <span class=\"hljs-type\">Double<\/span>)]\n  \n  <span class=\"hljs-keyword\">var<\/span> body: some <span class=\"hljs-type\">View<\/span> {\n    <span class=\"hljs-type\">ZStack<\/span> {\n      <span class=\"hljs-type\">GeometryReader<\/span> { geometry <span class=\"hljs-keyword\">in<\/span>\n        <span class=\"hljs-type\">Path<\/span> { path <span class=\"hljs-keyword\">in<\/span>\n          <span class=\"hljs-keyword\">let<\/span> xScale = geometry.size.width \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>)\n          <span class=\"hljs-keyword\">let<\/span> yScale = geometry.size.height \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }?.<span class=\"hljs-number\">1<\/span> ?? <span class=\"hljs-number\">1<\/span>)\n          path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: <span class=\"hljs-number\">0<\/span>, y: geometry.size.height - <span class=\"hljs-type\">CGFloat<\/span>(data&#91;<span class=\"hljs-number\">0<\/span>].<span class=\"hljs-number\">1<\/span>) * yScale))\n          <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-number\">1<\/span>..&lt;data.<span class=\"hljs-built_in\">count<\/span> {\n            path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: <span class=\"hljs-type\">CGFloat<\/span>(i) * xScale, y: geometry.size.height - <span class=\"hljs-type\">CGFloat<\/span>(data&#91;i].<span class=\"hljs-number\">1<\/span>) * yScale))\n          }\n        }\n        .stroke(<span class=\"hljs-type\">Color<\/span>.blue, lineWidth: <span class=\"hljs-number\">2<\/span>)\n        \n        <span class=\"hljs-comment\">\/\/\t\t\t\t\/\/ x-axis<\/span>\n        <span class=\"hljs-type\">Path<\/span> { path <span class=\"hljs-keyword\">in<\/span>\n          <span class=\"hljs-keyword\">let<\/span> xScale = geometry.size.width \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>)\n          <span class=\"hljs-keyword\">let<\/span> y = geometry.size.height\n          path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: <span class=\"hljs-number\">0<\/span>, y: y))\n          path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: geometry.size.width, y: y))\n          \n          <span class=\"hljs-keyword\">let<\/span> tickSpacing = <span class=\"hljs-number\">1<\/span>\n          <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-number\">0<\/span>..&lt;(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>) {\n            <span class=\"hljs-keyword\">let<\/span> tickX = <span class=\"hljs-type\">CGFloat<\/span>(i * tickSpacing) * xScale\n            <span class=\"hljs-keyword\">guard<\/span> tickX &lt; geometry.size.width <span class=\"hljs-keyword\">else<\/span> { <span class=\"hljs-keyword\">break<\/span> }\n            path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: tickX, y: y - <span class=\"hljs-number\">5<\/span>))\n            path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: tickX, y: y + <span class=\"hljs-number\">5<\/span>))\n          }\n        }\n        .stroke(<span class=\"hljs-type\">Color<\/span>.red, lineWidth: <span class=\"hljs-number\">5<\/span>)\n        <span class=\"hljs-comment\">\/\/<\/span>\n        <span class=\"hljs-comment\">\/\/ y-axis<\/span>\n        <span class=\"hljs-type\">Path<\/span> { path <span class=\"hljs-keyword\">in<\/span>\n          <span class=\"hljs-keyword\">let<\/span> x: <span class=\"hljs-type\">CGFloat<\/span> = <span class=\"hljs-number\">0<\/span>\n          <span class=\"hljs-keyword\">let<\/span> yScale: <span class=\"hljs-type\">CGFloat<\/span> = geometry.size.height \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }?.<span class=\"hljs-number\">1<\/span> ?? <span class=\"hljs-number\">1<\/span>)\n          path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: x, y: <span class=\"hljs-number\">0<\/span>))\n          path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: x, y: geometry.size.height))\n          \n          <span class=\"hljs-keyword\">let<\/span> tickSpacing: <span class=\"hljs-type\">CGFloat<\/span> = <span class=\"hljs-number\">1<\/span>\n          <span class=\"hljs-keyword\">for<\/span> i <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-number\">0<\/span>..&lt;<span class=\"hljs-type\">Int<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }!.<span class=\"hljs-number\">1<\/span>) {\n            <span class=\"hljs-keyword\">let<\/span> tickY = <span class=\"hljs-type\">CGFloat<\/span>(i) * tickSpacing * yScale\n            path.move(to: .<span class=\"hljs-keyword\">init<\/span>(x: x - <span class=\"hljs-number\">5<\/span>, y: geometry.size.height - tickY))\n            path.addLine(to: .<span class=\"hljs-keyword\">init<\/span>(x: x + <span class=\"hljs-number\">5<\/span>, y: geometry.size.height - tickY))\n          }\n        }\n        .stroke(<span class=\"hljs-type\">Color<\/span>.red, lineWidth: <span class=\"hljs-number\">5<\/span>)\n        \n        <span class=\"hljs-comment\">\/\/ add x-axis tick labels<\/span>\n        <span class=\"hljs-type\">ForEach<\/span>(<span class=\"hljs-number\">1<\/span>..&lt;(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>), id: \\.<span class=\"hljs-keyword\">self<\/span>) { i <span class=\"hljs-keyword\">in<\/span>\n          <span class=\"hljs-keyword\">let<\/span> tickLabel = <span class=\"hljs-type\">Text<\/span>(<span class=\"hljs-string\">\"\\(i)\"<\/span>)\n            .frame(width: <span class=\"hljs-number\">16<\/span>, height: <span class=\"hljs-number\">16<\/span>, alignment: .center)\n          <span class=\"hljs-keyword\">let<\/span> tickX = <span class=\"hljs-type\">CGFloat<\/span>(i) * geometry.size.width \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">count<\/span> - <span class=\"hljs-number\">1<\/span>)\n          tickLabel\n            .position(x: tickX, y: geometry.size.height - <span class=\"hljs-number\">16<\/span>)\n        }\n        \n        <span class=\"hljs-comment\">\/\/ add y-axis tick labels<\/span>\n        <span class=\"hljs-type\">ForEach<\/span>(<span class=\"hljs-number\">1<\/span>..&lt;<span class=\"hljs-type\">Int<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }!.<span class=\"hljs-number\">1<\/span>), id: \\.<span class=\"hljs-keyword\">self<\/span>) { i <span class=\"hljs-keyword\">in<\/span>\n          <span class=\"hljs-keyword\">let<\/span> tickLabel = <span class=\"hljs-type\">Text<\/span>(<span class=\"hljs-string\">\"\\(i)\"<\/span>)\n            .frame(width: <span class=\"hljs-number\">16<\/span>, height: <span class=\"hljs-number\">16<\/span>, alignment: .center)\n          <span class=\"hljs-keyword\">let<\/span> tickY = <span class=\"hljs-type\">CGFloat<\/span>(i) * geometry.size.height \/ <span class=\"hljs-type\">CGFloat<\/span>(data.<span class=\"hljs-built_in\">max<\/span> { $<span class=\"hljs-number\">0.1<\/span> &lt; $<span class=\"hljs-number\">1.1<\/span> }!.<span class=\"hljs-number\">1<\/span>)\n          tickLabel\n            .position(x: <span class=\"hljs-number\">16<\/span> - <span class=\"hljs-number\">5<\/span>, y: geometry.size.height - tickY)\n        }\n      }\n    }\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Swift<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">swift<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>","protected":false},"excerpt":{"rendered":"<p>Graphs and charts are an essential tool for visualizing data, and with SwiftUI, it&#8217;s easy to create custom and interactive graphs (Spoiler: we won&#8217;t add any interactivity during this arcticle). You can use them in iOS, iPadOS, and macOS apps. In this article, we&#8217;ll explore how to draw a line graph in SwiftUI. In a&hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[3],"tags":[62,4,61],"class_list":["post-709","post","type-post","status-publish","format-standard","hentry","category-ios-development","tag-graphs","tag-ios","tag-swiftui"],"_links":{"self":[{"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/posts\/709","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/comments?post=709"}],"version-history":[{"count":10,"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/posts\/709\/revisions"}],"predecessor-version":[{"id":868,"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/posts\/709\/revisions\/868"}],"wp:attachment":[{"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/media?parent=709"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/categories?post=709"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mic.st\/blog\/wp-json\/wp\/v2\/tags?post=709"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}