Is there a way to observe the content offset of a scrolling SwiftUI Chart?

122 views Asked by At

I need to create a component which enables the users to select a value in a specific date by horizontally scrolling the SwiftUI charts. The image of the component is below.

enter image description here

My code block is as below:

struct SampleModel: Identifiable {
    var id = UUID()
    var date: String
    var value: Double
    var animate: Bool = false
}

struct ContentView: View {
    @State private var data = [
        SampleModel(date: "26\nFr", value: 9.2),
        SampleModel(date: "27\nSa", value: 12.5),
        SampleModel(date: "28\nSu", value: 15.0),
        SampleModel(date: "29\nMo", value: 20.0),
        SampleModel(date: "30\nTu", value: 5.0),
        SampleModel(date: "31\nWe", value: 7.0),
        SampleModel(date: "01\nTh", value: 3.0),
        SampleModel(date: "02\nFr", value: 20.0),
        SampleModel(date: "03\nSa", value: 5.0),
        SampleModel(date: "04\nSu", value: 7.0),
        SampleModel(date: "05\nMo", value: 3.0),
        SampleModel(date: "06\nTu", value: 10.0),
        SampleModel(date: "07\nWe", value: 21.0),
        SampleModel(date: "08\nTh", value: 14.0),
        SampleModel(date: "09\nFr", value: 10.0),
        SampleModel(date: "10\nSt", value: 7.0),
        SampleModel(date: "11\nSu", value: 15.0),
        SampleModel(date: "12\nMo", value: 17.0),
        SampleModel(date: "13\nTu", value: 29.0)
    ]
    @State private var scrollPosition: String = "26\nFr"
    @State var priceText: String = ""

    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text(priceText)
                    .padding()

                Chart(data) { flight in
                    BarMark(x: .value("Date", flight.date),
                            y: .value("Price", flight.value),
                            width: 10.0)
                    .foregroundStyle(
                        Gradient(
                            colors: [
                                .blue,
                                .green
                            ]
                        )
                    )
                    .clipShape(RoundedRectangle(cornerRadius: 16))
                }
                .frame(width: .infinity, height: 180)
                .background(content: {
                    VStack {
                        Color.gray.frame(width: 1)
                        Color.clear.frame(height: 40)
                    }
                })
                .chartXAxis(content: {
                    AxisMarks(preset: .extended, position: .bottom) { value in
                        let label = value.as(String.self)!
                        AxisValueLabel(label)
                            .foregroundStyle(.gray)
                    }
                })
                .chartScrollableAxes(.horizontal)
                .chartXVisibleDomain(length: 11)
                .chartYAxis(.hidden)
                .chartScrollTargetBehavior(
                    .valueAligned(unit: 1)
                )
                .chartOverlay { proxy in
                    
                }
            }
            .frame(width: .infinity)

        }
    }
}

I need to observe the content offset of the horizontally scrolling chart in order to retrieve the centre value based on the position by using proxy of chartOverlay. Is there a way to observe the content offset of the scrolling chart? Or is there another perspective to retrieve the centre value in the scrolling chart?

1

There are 1 answers

4
Reinier Melian On BEST ANSWER

Following your approach, I added some improvements, I added margin on the left and right side, allowing a proper selection when scrolling, also defined a scrollPosition which is Int to track the index selected of your data.

If you want to change the value shown on top you only need to modify the selectionText function to return .value if you need

hope this is what you are asking for.

enter image description here

import SwiftUI
import Charts

struct SampleModel: Identifiable {
    var id = UUID()
    var date: String
    var value: Double
    var animate: Bool = false
}

struct ContentView: View {
    @State private var data = [
        SampleModel(date: "26\nFr", value: 9.2),
        SampleModel(date: "27\nSa", value: 12.5),
        SampleModel(date: "28\nSu", value: 15.0),
        SampleModel(date: "29\nMo", value: 20.0),
        SampleModel(date: "30\nTu", value: 5.0),
        SampleModel(date: "31\nWe", value: 7.0),
        SampleModel(date: "01\nTh", value: 3.0),
        SampleModel(date: "02\nFr", value: 20.0),
        SampleModel(date: "03\nSa", value: 5.0),
        SampleModel(date: "04\nSu", value: 7.0),
        SampleModel(date: "05\nMo", value: 3.0),
        SampleModel(date: "06\nTu", value: 10.0),
        SampleModel(date: "07\nWe", value: 21.0),
        SampleModel(date: "08\nTh", value: 14.0),
        SampleModel(date: "09\nFr", value: 10.0),
        SampleModel(date: "10\nSt", value: 7.0),
        SampleModel(date: "11\nSu", value: 15.0),
        SampleModel(date: "12\nMo", value: 17.0),
        SampleModel(date: "13\nTu", value: 29.0)
    ]
    @State private var scrollPosition: Int = 0
    @State var priceText: String? = nil
    @State var selectedIndex: Int = 0

    var body: some View {
        GeometryReader { geometry in
            VStack {
                if let price = priceText {
                    Text(price)
                        .padding()
                }

                    Text(selectionText())
                        .padding()

                Chart(Array(zip(data.indices, data)), id: \.0) { index, flight in
                    BarMark(x: .value("Index", index),
                            y: .value("Price", flight.value), width: .fixed(10))
                    .foregroundStyle(
                        Gradient(
                            colors: [
                                .blue,
                                .green
                            ]
                        )
                    )
                    .clipShape(RoundedRectangle(cornerRadius: 16))
                }
                .frame(height: 180)
                .background(content: {
                    VStack {
                        Color.gray.frame(width: 1)
                        Color.clear.frame(height: 40)
                    }
                })
                .chartXAxis(content: {
                    AxisMarks(preset: .aligned, position: .bottom, values: .stride(by: 1)) { value in
                        let label = data[value.index].date
                        AxisValueLabel(label)
                            .foregroundStyle(.gray)
                    }
                })
                .chartXVisibleDomain(length: 10)
                .chartYAxis(.hidden)
                .chartScrollTargetBehavior(
                    .valueAligned(unit: 1)
                )
                .chartScrollableAxes(.horizontal)
                .chartScrollPosition(x: $scrollPosition)
                .contentMargins(Edge.Set(arrayLiteral: [.leading, .trailing]), geometry.size.width/2)
            }
        }
    }

    private func selectionText() -> String {
        guard scrollPosition >= .zero else {
            return data[.zero].date
        }

        guard scrollPosition < data.count else {
            return data[data.count - 1].date
        }

        return data[scrollPosition].date
    }
}