Home > Mobile >  SwiftUI Charts tapping Bar Chart
SwiftUI Charts tapping Bar Chart

Time:11-05

I have used the new SwiftUI Charts to make a simple bar chart, with month names on the x axis and cost on the y axis. I have added a .chartOverlay view modifier which tracks where I tap on the chart, and shows a BarMark with a text of the cost of the bar tapped.

This was very straightforward to implement, but when starting tapping on the bars, I found that tapping on the first and second bar produced the wanted result, but starting with the third bar, the bar tapped and the second bar was swapped, but the BarMark text showed was correct.

I have used a few hours on this problem, but can't find out what is wrong here.

The complete code is shown here, if anyone can see what is wrong:

import SwiftUI
import Charts

final class ViewModel: ObservableObject {
    let months = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr"]
    let costs = [20.0, 40.0, 80.0, 60.0, 75.0, 30.0]
    @Published var forecast: [Forecast] = []
    @Published var selectedMonth: String?
    @Published var selectedCost: Double?

    init() {
        forecast = months.indices.map { .init(id: UUID(), month: months[$0], cost: costs[$0]) }
    }
}

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        Chart(viewModel.forecast) { data in
            BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
                .foregroundStyle(Color.blue)
            if let selectedMonth = viewModel.selectedMonth, let selectedCost = viewModel.selectedCost {
                RuleMark(x: .value("Selected month", selectedMonth))
                    .annotation(position: .top, alignment: .top) {
                        VStack {
                            Text(estimated(for: selectedMonth, and: selectedCost))
                        }
                    }
            }
        }
        .chartYAxis {
            AxisMarks(position: .leading)
        }
        .chartOverlay { proxy in
            GeometryReader { geometry in
                ZStack(alignment: .top) {
                    Rectangle().fill(.clear).contentShape(Rectangle())
                        .onTapGesture { location in
                            updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
                        }
                }
            }
        }
    }
    
    func updateSelectedMonth(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
        let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x
        guard let month: String = proxy.value(atX: xPosition) else {
            return
        }
        viewModel.selectedMonth = month
        viewModel.selectedCost = viewModel.forecast.first(where: { $0.month == month })?.cost
    }

    func estimated(for month: String, and cost: Double) -> String {
        let estimatedString = "estimated: \(cost)"
        return estimatedString
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .padding()
    }
}

struct Forecast: Identifiable {
    let id: UUID
    let month: String
    let cost: Double
}

CodePudding user response:

try this approach, using a ForEach loop and having the RuleMark after it:

var body: some View {
    Chart { // <-- here
        ForEach(viewModel.forecast) { data in  // <-- here
            BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
                .foregroundStyle(Color.blue)
        }  // <-- here
        
        if let selectedMonth = viewModel.selectedMonth,
           let selectedCost = viewModel.selectedCost {
            RuleMark(x: .value("Selected month", selectedMonth))
                .annotation(position: .top, alignment: .top) {
                    VStack {
                        Text(estimated(for: selectedMonth, and: selectedCost))
                    }
                }
        }
    }
    .chartYScale(domain: 0...100) // <-- here
    .chartYAxis {
        AxisMarks(position: .leading)
    }
    .chartOverlay { proxy in
        GeometryReader { geometry in
            ZStack(alignment: .top) {
                Rectangle().fill(.clear).contentShape(Rectangle())
                    .onTapGesture { location in
                        updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
                    }
            }
        }
    }
}
  • Related