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