Search code examples
iosswiftswiftuichartsswiftui-charts

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


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?


Solution

  • 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
        }
    }