SwiftUI BarChart with Drag Gesture

DevTechie Inc
Jun 17, 2022


Photo by Luke Chesser on Unsplash

Today we will build another bar chart but this time we will use drag gesture to reveal the chart value. Our final output will look like this:

For this example, we will once again use dummy data representing values of monthly expense. This is how our model and sample data will look like:

struct ChartData: Identifiable {
    var id = UUID().uuidString
    var data: CGFloat
    var month: String
    var color: Color
}extension ChartData {
    static var sampleData: [ChartData] {
        [
            ChartData(data: 235, month: "JAN", color: .purple),
            ChartData(data: 500, month: "FEB", color: .purple),
            ChartData(data: 400, month: "MAR", color: .purple),
            ChartData(data: 350, month: "APR", color: .purple),
            ChartData(data: 625, month: "MAY", color: .purple),
            ChartData(data: 375, month: "JUN", color: .purple),
            ChartData(data: 475, month: "JUL", color: .purple),
            ChartData(data: 600, month: "AUG", color: .purple),
            ChartData(data: 321, month: "SEP", color: .purple),
            ChartData(data: 100, month: "OCT", color: .purple),
            ChartData(data: 674, month: "NOV", color: .purple),
            ChartData(data: 456, month: "DEC", color: .purple),
        ]
    }
}
ChartData model contains color property so you can apply different colors for your use case. I will set color to be purple for this example.

We will need state variables along with GestureState variable to track drag state. We will expect to receive chartData while this view is being created.

var chartData: [ChartData]
    
    @GestureState var isDragging: Bool = false
    @State var offset: CGFloat = 0
    @State var currentMonthID: String = ""
Next we will create label for our chart as shown below:

@ViewBuilder
func chartLabel(chartData: ChartData, label: String) -> some View {
    Text(label)
        .font(.system(size: 10))
        .foregroundColor(isDragging && currentMonthID == chartData.id ? chartData.color : .gray)
}
After that, we will create a single bar in the bar chart. We will use RoundedRectangle for this purpose. This bar will have Text view as an overlay which will only be shown when user’s drag point is somewhere within the bar.

@ViewBuilder
func chartColumn(chartData: ChartData, proxySize: CGSize) -> some View {
    RoundedRectangle(cornerRadius: 6)
        .fill(currentMonthID == chartData.id ? chartData.color : .gray)
        .opacity(isDragging ? (currentMonthID == chartData.id ? 1 : 0.5) : 1)
        .frame(height: (chartData.data / maxVal()) * (proxySize.height))
        .overlay (
            Text("\(Int(chartData.data))")
                .font(.system(size: 10))
                .foregroundColor(chartData.color)
                .opacity(isDragging && currentMonthID == chartData.id ? 1 : 0)
                .offset(y: -30)
            ,alignment: .top
        )
        .frame(maxHeight: .infinity,alignment: .bottom)
}
To normalize our chart’s height we will use a function to get max out of the chart’s data:

func maxVal() -> CGFloat {
        let max = chartData.max { first, second in
            return second.data > first.data
        }
        return max?.data ?? 0
    }
We will use GestureState to track the dragging state along with computed offset to mark the location of current point of touch while drag is changing. But before all that we will need a chart cell to combine label and bar together as shown below:

@ViewBuilder
func chartCell(chartData: ChartData) -> some View {
    VStack(spacing: 10) {
        GeometryReader { proxy in
            chartColumn(chartData: chartData, proxySize: proxy.size)
        }
        chartLabel(chartData: chartData, label: chartData.month)
    }
}
Now we will create chart view to combine chart cells along with drag gesture:

@ViewBuilder
    func chart() -> some View {
        HStack(spacing: 10) {
            ForEach(chartData) { currentData in
                chartCell(chartData: currentData)
            }
        }
        .frame(height: 150)
        .animation(.easeOut, value: isDragging)
        .gesture(
            DragGesture()
                .updating($isDragging, body: { _, out, _ in
                    out = true
                })
                .onChanged({ value in
                    offset = isDragging ? value.location.x : 0
                    let perBlock = (UIScreen.main.bounds.width - 20) / CGFloat(chartData.count)
                    let index = max(min(Int(offset / perBlock), chartData.count - 1), 0)
                    self.currentMonthID = chartData[index].id
                })
                .onEnded({ value in
                    withAnimation{
                        offset = .zero
                        currentMonthID = ""
                }
            })
        )
    }
Complete code will look like this:

import SwiftUIstruct DragableBarChart: View {
    var chartData: [ChartData]
    
    @GestureState var isDragging: Bool = false
    @State var offset: CGFloat = 0
    @State var currentMonthID: String = ""
    
    var body: some View {
        VStack {
            chart()
                .padding(.horizontal, 20)
               // .background(RoundedRectangle(cornerRadius: 10).fill(Color.orange.opacity(0.6)))
        }
    }
    
    
    
    func maxVal() -> CGFloat {
        let max = chartData.max { first, second in
            return second.data > first.data
        }
        return max?.data ?? 0
    }
    
    @ViewBuilder
    func chart() -> some View {
        HStack(spacing: 10) {
            ForEach(chartData) { currentData in
                chartCell(chartData: currentData)
            }
        }
        .frame(height: 150)
        .animation(.easeOut, value: isDragging)
        .gesture(
            DragGesture()
                .updating($isDragging, body: { _, out, _ in
                    out = true
                })
                .onChanged({ value in
                    offset = isDragging ? value.location.x : 0
                    let perBlock = (UIScreen.main.bounds.width - 20) / CGFloat(chartData.count)
                    let index = max(min(Int(offset / perBlock), chartData.count - 1), 0)
                    self.currentMonthID = chartData[index].id
                })
                .onEnded({ value in
                    withAnimation{
                        offset = .zero
                        currentMonthID = ""
                }
            })
        )
    }
    
    @ViewBuilder
    func chartCell(chartData: ChartData) -> some View {
        VStack(spacing: 10) {
            GeometryReader { proxy in
                chartColumn(chartData: chartData, proxySize: proxy.size)
            }
            chartLabel(chartData: chartData, label: chartData.month)
        }
    }
    
    @ViewBuilder
    func chartColumn(chartData: ChartData, proxySize: CGSize) -> some View {
        RoundedRectangle(cornerRadius: 6)
            .fill(currentMonthID == chartData.id ? chartData.color : .gray)
            .opacity(isDragging ? (currentMonthID == chartData.id ? 1 : 0.5) : 1)
            .frame(height: (chartData.data / maxVal()) * (proxySize.height))
            .overlay (
                Text("\(Int(chartData.data))")
                    .font(.system(size: 10))
                    .foregroundColor(chartData.color)
                    .opacity(isDragging && currentMonthID == chartData.id ? 1 : 0)
                    .offset(y: -30)
                ,alignment: .top
            )
            .frame(maxHeight: .infinity,alignment: .bottom)
    }
    
    @ViewBuilder
    func chartLabel(chartData: ChartData, label: String) -> some View {
        Text(label)
            .font(.system(size: 10))
            .foregroundColor(isDragging && currentMonthID == chartData.id ? chartData.color : .gray)
    }
}struct DragableBarChart_Previews: PreviewProvider {
    static var previews: some View {
        DragableBarChart(chartData: ChartData.sampleData)
    }
}struct ChartData: Identifiable {
    var id = UUID().uuidString
    var data: CGFloat
    var month: String
    var color: Color
}extension ChartData {
    static var sampleData: [ChartData] {
        [
            ChartData(data: 235, month: "JAN", color: .purple),
            ChartData(data: 500, month: "FEB", color: .purple),
            ChartData(data: 400, month: "MAR", color: .purple),
            ChartData(data: 350, month: "APR", color: .purple),
            ChartData(data: 625, month: "MAY", color: .purple),
            ChartData(data: 375, month: "JUN", color: .purple),
            ChartData(data: 475, month: "JUL", color: .purple),
            ChartData(data: 600, month: "AUG", color: .purple),
            ChartData(data: 321, month: "SEP", color: .purple),
            ChartData(data: 100, month: "OCT", color: .purple),
            ChartData(data: 674, month: "NOV", color: .purple),
            ChartData(data: 456, month: "DEC", color: .purple),
        ]
    }
}
Output:

With that we have reached the end of this article. Thank you once again for reading. Don’t forget to subscribe our weekly newsletter at https://www.devtechie.com