Charts in SwiftUI – Part 1: Bar Chart

Share on facebook
Share on twitter
Share on pinterest

by Ahmed Mgua (@_mgua)

Charts are important visual elements that help present data to users in a meaningful way. Depending on the type of data, you may decide to use any of the common chart types, for example, bar charts, pie charts, and line charts. In this tutorial series, we will go over how to create the different types of charts in SwiftUI apps to help visualize different types of data.

Today, we start with creating a bar chart in SwiftUI! Bar charts are the simplest type of charts to create in SwiftUI. In UI terms, a bar chart is simply a row of rectangles each with a height corresponding to the value it represents and a text label showing what the value means.

Here’s a sample of the SwiftUI bar chart we are creating:


After this tutorial, you should be able to implement your own custom bar chart for your SwiftUI apps. Let’s get started by creating a new Xcode App project and naming it, for instance, “MyBarChart”.

Define the look of a bar 🍫

First, we need to define how a chart bar chart cell should look like. For this purpose, create a new File – New – SwiftUI View file and name it “BarChartCell”. Then replace the pre-generated Text view with this:

struct BarChartCell: View {       
                
    var value: Double               
    var barColor: Color       
                         
    var body: some View {       
        RoundedRectangle(cornerRadius: 5)       
            .fill(barColor)       
            .scaleEffect(CGSize(width: 1, height: value), anchor: .bottom)       
                
    }       
}

Let’s go over the code in our BarChartCell. A bar in our bar chart is a simple rounded Rectangle that takes two arguments, a value which is a Double, and a barColor which is a Color. The barColor is used to fill the Rectangle with the color of choice. The value is used to determine the height of the rectangle. We use the .scaleEffect modifier here to adjust the height of the Rectangle based on the value the cell represents, and set the anchor to .bottom. This means that each bar will be anchored to the bottom of the frame in the chart, which is the expected behavior for a bar chart.

Next, feed the BarChartCell_Previews with some sample data. Also, clip the size of the preview to the frame of the BarChartCell.

struct BarChartCell_Previews: PreviewProvider {
     static var previews: some View {
         BarChartCell(value: 3800, barColor: .blue)
             .previewLayout(.sizeThatFits)
     }
 }

And this is how a single BarChartCell should look like:

The basic capabilities of our bar chart 📊

Next, create let’s create the view that actually contains our bar chart by creating a new File-New-File and naming it “BarChart”.

The layout of our chart is simple, we have a vertical stack of the chart title at the top, the current value, and the row of bars showing our values.

Our SwiftUI bar chart should be capable of the following things:

  • The bars can sense when they are touched or dragged along
  • The current value updates itself to the value for the currently touched bar
  • There is a label that shows the label of the currently touched bar and moves to stay below the touched bar, which is replaced by the legend when the bar is no longer being touched.

Let’s start building such bar chart in SwiftUI step-by-step.

First, we need to declare three customization properties and one data set property.

import SwiftUI               
                
struct BarChart: View {    
              
    var title: String                  
    var legend: String                   
    var barColor: Color                 
    var data: [ChartData]        
                         
    var body: some View {                    
      Text("Hello, World!")                   
    }       
}

By using those, we can set the title of the chart and the color of the bars to pass down to our BarChartCell. We also pass the data we are using with this bar chart as an array of “ChartData”.

Preparing our data set 💻

“ChartData” should be a custom type we can create to represent values that can be labeled. Let’s create a new File-New-Swift file called “ChartData” to create this model.

struct ChartData {
     var label: String
     var value: Double
 }

Right below our ChartData struct, we can create an array holding some sample ChartData entries.

let chartDataSet = [
     ChartData(label: "January 2021", value: 340.32),
     ChartData(label: "February 2021", value: 250.0),
     ChartData(label: "March 2021", value: 430.22),
     ChartData(label: "April 2021", value: 350.0),
     ChartData(label: "May 2021", value: 450.0),
     ChartData(label: "June 2021", value: 380.0),
     ChartData(label: "July 2021", value: 365.98)
 ]

We can now feed our BarChart_Previews struct with the necessary information including our sample chartDataSet.

struct BarChart_Previews: PreviewProvider {
     static var previews: some View {
         BarChart(title: "Monthly Sales", legend: "EUR", barColor: .blue, data: chartDataSet)
     }
 }

Let’s jump back to our BarChart view. Next, we insert two @State properties for showing the value and label of the bar being currently touched.

@State private var currentValue = ""
@State private var currentLabel = ""

Let’s move on to the body of the BarChart view.

Here, we need to insert a VStack which contains the Text labels for the chart title and the current value State for each BarChartCell initialized.

var body: some View {
         VStack(alignment: .leading) {
             Text(title)
                 .bold()
                 .font(.largeTitle)
             Text("Current value: (currentValue)")
                 .font(.headline)
         }
             .padding()
}

This is how your BarChart view should look so far:

The VStack should also contain a GeometryReader in which we will render the chart.

VStack(alignment: .leading) {                   
    //…                 
    GeometryReader { geometry in       
                
    }                      
}                               
    .padding()

GeometryReader is a container view that has quite a lot of tricks up its sleeve, but for this tutorial, we will use it to find the size of the frame in which the BarChart is rendered.

Determining the height of each bar 📏

Inside the GeometryReader, we add another VStack which contains the HStack of our cells and the moving label of the currently touched bar.

GeometryReader { geometry in
     VStack {
         HStack {
             //Cells
         }
         if currentLabel.isEmpty {
             Text(legend)
                 .bold()
                 .foregroundColor(.black)
                 .padding(5)
                 .background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white).shadow(radius: 3))
         } else {
             Text(currentLabel)
                 .bold()
                 .foregroundColor(.black)
                 .padding(5)
                 .background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white).shadow(radius: 3))
                  .animation(.easeIn)
         }
     }
}

This is how our BarChart looks so far:

Let’s use a ForEach loop, to iterate over the indices of our array of data. This gives us index 0 through the last index of our data array, so we can use that index in a number of ways.

HStack {                
     ForEach(0..<data.count, id: \.self) { i in       
          
     }                        
}

For each index, we will first instantiate a BarChartCell, giving it a normalized value and the barColor we made. This gives us the varying heights of the bars corresponding to their real values.

To obtain the normalized values, insert the following function below your BarChart‘s body:

func normalizedValue(index: Int) -> Double {
         var allValues: [Double]    {
             var values = [Double]()
             for data in data {
                 values.append(data.value)
             }
             return values
         }
         guard let max = allValues.max() else {
             return 1
         }
         if max != 0 {
             return Double(data[index].value)/Double(max)
         } else {
             return 1
         }
}

This function assigns a value of 1 to the maximum value in our array, and then gets the ratio of each remaining value to our maximum. Therefore, in an array of [30, 50, 100, 75, 60], 100 will be returned as 1, while 30 is returned as 30/100, 50 returned as 50/100, and so on.

We can now use our normalizedValue function to set the height of each BarChartCell using the value parameter:

ForEach(0..<data.count, id: \.self) { i in
    BarChartCell(value: normalizedValue(index: i), barColor: barColor)
        .animation(.spring())
        .padding(.top)
}

Awesome, our BarChart is slowly taking shape! Take a look at how the BarChartCell‘s are generated using normalized values:

Highlighting the selected bar 💡

Last but not least, we want to highlight a certain bar once the user touches it. To do this, we first need to determine whether a bar is touched and then provide a suitable visual cue for that.

For this purpose, let’s add another State property for keeping track of the location on the screen currently being touched.

@State private var touchLocation: CGFloat = -1

Next, we need to create a function that will sense a bar being touched by checking that the touchLocation is greater than the current bar’s beginning but less than the next bar’s beginning and return a boolean value. Let’s insert this function below our normalizedValue function.

func barIsTouched(index: Int) -> Bool {
    touchLocation > CGFloat(index)/CGFloat(data.count) && touchLocation < CGFloat(index+1)/CGFloat(data.count)
}

We can then combine this function with fitting view modifiers and use the return boolean value to perform a myriad of visual changes. For example, we can use the .scaleEffect and .opacity modifier to make the bar slightly wider when it is touched. Of course, you can use anything that you feel would be fitting.

BarChartCell(value: normalizedValue(index: i), barColor: barColor)
    .opacity(barIsTouched(index: i) ? 1 : 0.7)
    .scaleEffect(barIsTouched(index: i) ? CGSize(width: 1.05, height: 1) : CGSize(width: 1, height: 1), anchor: .bottom)
    .animation(.spring())
    .padding(.top)

Okay, we know have the capability to know whether a certain bar is touched and to tell this specific BarChartCell to perform some visual changes when being touched.

Detecting the user’s gestures 👉

To make the highlight feature work, we need to know which bar is touched. To do this, we need to use the index of the current bar and the touchLocation State property to determine where on the screen the user is touching. For this purpose, we attach a .gesture modifier to our HStack containing the BarChartCell‘s while using the DragGesture. We use a minimum distance of zero. This means that the drag gesture will be activated for a tap as well.

HStack {                     
    //…                     
}                  
    .gesture(DragGesture(minimumDistance: 0) 
      
    )

The DragGesture comes with two modifiers of interest to us: the .onChanged and .onEnded modifiers. We use them to write code that runs when the drag gesture is activated and when it stops. Both of them come with a closure argument that gives you the position of the drag gesture or where it stopped to use in the closure.

.gesture(DragGesture(minimumDistance: 0)                
    .onChanged({ position in       
         
    })                                       
    .onEnded({ position in                                
                
    })                            
)

This position comes with a location attribute, which we can check to know the location coordinates of the drag gesture on the screen. However, we only want to know about the touches inside the frame where the bar chart is.

Here is where we can see a little bit of the magic of GeometryReader.
The GeometryReader provides us with a frame(in: _) function that reads the size of the frame we give it. We can then use that frame size to determine the touch or drag position along the axis of the given frame, but not anything outside it.

.onChanged({ position in
     let touchPosition = position.location.x/geometry.frame(in: .local).width
 })

For the .onChanged modifier, we need the obtained touchPosition to update the current value of the touchLocation State.

.onChanged({ position in               
    let touchPosition = position.location.x/geometry.frame(in: .local).width       
                        
    touchLocation = touchPosition                                
})

The touchLocation is a value between 0 and 1 with 0 being the left edge of the frame and 1 being the right edge of the frame. We create a function to update the current value, which will in turn update the UI
accordingly. Let’s insert such a function below our barIsTouched function,

func updateCurrentValue()    {
         let index = Int(touchLocation * CGFloat(data.count))
         guard index < data.count && index >= 0 else {
             currentValue = ""
             currentLabel = ""
             return
         }
         currentValue = "\(data[index].value)"
         currentLabel = data[index].label
     }

The updateCurrentValue function uses the value of the touchLocation to find the index of the value in our data array. So for a touchLocation of 0.5, our index will be the halfway index in the data array. We need to be careful here to make sure that the index does not go beyond the first or last index in our data array, so we use a guard statement to keep that in check.

Let’s use this function to complete the .onChanged closure.

.onChanged({ position in
    let touchPosition = position.location.x/geometry.frame(in: .local).width
     touchLocation = touchPosition    
    updateCurrentValue()                                                                
})

For the .onEnded modifier, we can omit the position argument since we will simply reset the states to show that the drag gesture has ended and the visual cues can stop. Let’s set the touchLocation to a negative value here to mean “the user is touching outside the screen”. To make the transition more sensible, we can reset the values after a short delay, say 0.5s.

.onEnded({ _ in                 
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {              
        withAnimation(Animation.easeOut(duration: 0.5)) {        
            touchLocation = -1          
            currentValue  =  ""       
            currentLabel = ""           
        }                 
     }       
})

Let’s outsource the code into its own function as well ..

func resetValues() {
         touchLocation = -1
         currentValue  =  ""
         currentLabel = ""
}

… and call it from the .onEnded closure like this:

.onEnded({ _ in                  
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {               
        withAnimation(Animation.easeOut(duration: 0.5)) {            
            resetValues()              
        }                 
    }       
})

Finally, for a little flair, we can also use the touchLocation to offset the label for the current bar using a new function that takes the width of the frame and uses it to determine the position the label should be displayed. So let’s add this last function to the BarChart struct.

func labelOffset(in width: CGFloat) -> CGFloat {
         let currentIndex = Int(touchLocation * CGFloat(data.count))
         guard currentIndex < data.count && currentIndex >= 0 else {
             return 0
         }
         let cellWidth = width / CGFloat(data.count)
         let actualWidth = width -    cellWidth
         let position = cellWidth * CGFloat(currentIndex) - actualWidth/2
         return position
}

We can then pass in the function to an .offset modifier applied the label Text view:

Text(currentLabel)
    //…
    .offset(x: labelOffset(in: geometry.frame(in: .local).width))
    .animation(.easeIn)

Our SwiftUI bar chart is finally completed. Let’s run a live preview to see if everything works.

Conclusion 🎉

There we have it! We now have a simple bar chart that we can use in our SwiftUI apps, that can adjust according to different frames. Feel free to experiment!

In the next part, we’ll dive into pie charts in SwiftUI and see more of what GeometryReader can do, and start getting into Paths and Shapes in SwiftUI.

We’ve uploaded the source code for the app to GitHub.

You liked the tutorial, and you want to learn more about developing iOS apps with SwiftUI? Then check out our Interactive Mastering SwiftUI Book!

3 replies on “Charts in SwiftUI – Part 1: Bar Chart”

Thank you for the article! Very good stuff. There is, however, a problem. In the GitHub code, ContentView.swift remains with Text(“Hello, world!”). As a consequence, only “Hello, world!” displays unless updates to the code are made.

Why do I get « hello world » at the start? And that’s it 🙁

Do you somehow drop Content View??? I do not se it on your last photo!

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount