swiftswiftuicharts

Preserving original chart horizontal scroll while adding a new DragGesture


I am trying to add a chart overlay, and a custom DragGesture to not reset my variable called rawSelectedDate after stopping dragging.

The implementation is fine, but it does not allow the original horizontal scrolling of my chart, which I also need to preserve.

I tried adding simultaneousGesture(), but it didn't solve the problem. I also tried increasing the minimumDistance parameter in DragGesture, but that didn't work either.

The code:

struct ScoreChartModel: Identifiable {
    let id = UUID()
    var score: Int
    var date: Date
}

struct SleepScoreChart: View {
    @State var chartDisplayData: [ScoreChartModel]
    @Binding var rawSelectedDate: Date?
    
    var body: some View {
        Chart {
            ForEach(chartDisplayData) { chartData in
                PointMark(
                    x: .value("sleep date", chartData.date, unit: .day),
                    y: .value("sleep score", chartData.score)
                )
                .interpolationMethod(.catmullRom)
            }
        }
        .chartXAxis {
            AxisMarks(values: .stride(by: .day)) { _ in
                AxisTick()
                AxisGridLine()
                AxisValueLabel(format: .dateTime.weekday(.abbreviated))
            }
        }
        .chartYScale(domain: 0...120)
        .chartYAxis(.hidden)
        .chartScrollableAxes(.horizontal)
        .chartXVisibleDomain(length: 3600 * 24 * 7)
        .chartXSelection(value: $rawSelectedDate)
        .chartScrollPosition(initialX: Date())
        .chartOverlay { proxy in
            GeometryReader { geometry in
                Rectangle().fill(.clear).contentShape(Rectangle())
                    .simultaneousGesture(
                        DragGesture(minimumDistance: 0)
                            .onChanged { value in
                                if let plotFrame = proxy.plotFrame {
                                    let x = value.location.x - geometry[plotFrame].origin.x
                                    guard let date = proxy.value(atX: x, as: Date.self) else { return }
                                    let closestDataPoint = chartDisplayData.min { first, second in
                                        abs(first.date.timeIntervalSince(date)) < abs(second.date.timeIntervalSince(date))
                                    }
                                    if let closestDate = closestDataPoint?.date {
                                        rawSelectedDate = closestDate
                                    }
                                }
                            }
                    )
                    
            }
        }
    }
}

Solution

  • Sometimes you just have to take a break ... and come back with a fresh mind

    The solution is obvious:

    struct SleepScoreChart: View {
        @State var chartDisplayData: [ScoreChartModel]
        @Binding var rawSelectedDate: Date?
        @Binding var selectedDate: Date?
        
        var body: some View {
            Chart {
                ForEach(chartDisplayData) { chartData in
                    PointMark(
                        x: .value("sleep date", chartData.date, unit: .day),
                        y: .value("sleep score", chartData.score)
                    )
                    .interpolationMethod(.catmullRom)
                }
            }
            .chartXAxis {
                AxisMarks(values: .stride(by: .day)) { _ in
                    AxisTick()
                    AxisGridLine()
                    AxisValueLabel(format: .dateTime.weekday(.abbreviated))
                }
            }
            .chartYScale(domain: 0...120)
            .chartYAxis(.hidden)
            .chartScrollableAxes(.horizontal)
            .chartXVisibleDomain(length: 3600 * 24 * 7)
            .chartXSelection(value: $rawSelectedDate)
            .chartScrollPosition(initialX: Date())
            .onChange(of: rawSelectedDate) {
                if let date = rawSelectedDate {
                    selectedDate = date
                }
            }
        }
    }
    

    Set selectedDate only when rawSelectedDate is not nil, problem solved