简体   繁体   中英

SwiftUI Scrollable Charts in IOS16

Using the new SwiftUI Charts framework, we can make a chart bigger than the visible screen and place it into a ScrollView to make it scrollable. Something like this:

var body : some View {
    
    GeometryReader { proxy in

        ScrollView(.horizontal, showsIndicators: false) {

            Chart {

                ForEach(data) { entry in

                    // ...
                }
            }
            .frame(width: proxy.size.width * 2)
        }
    }
}

Does anybody know if it is possible to programmatically move the scroll to display a certain area of the chart?

I've tried using ScrollViewReader , setting the IDs at the x-axis labels, and trying to use the scrollTo function to navigate to any of those positions with no luck:

Chart {

    /// ...
    
}
.chartXAxis {

    AxisMarks(values: .stride(by: .day)) { value in
    
        if let date : Date = value.as(Date.self) {
            Text(date, style: .date)
                .font(.footnote)
        }
    }
}

This cheesy workaround seems to do the trick. I put the chart in a ZStack with an HStack overlaying the chart. The HStack contains a bunch of invisible objects that conform to the Identifiable protocol. The quantity, ids, and positions of the invisible objects match the charted data.

Since the ZStack view now contains identifiable elements, ScrollViewReader works as expected.

import SwiftUI
import Charts

struct ChartData: Identifiable {
    var day: Int
    var value: Int
    var id: String { "\(day)" }
}

struct ContentView: View {
    @State var chartData = [ChartData]()
    @State var scrollSpot = ""
    let items = 200
    let itemWidth: CGFloat = 30
    
    var body: some View {
        VStack {
            ScrollViewReader { scrollPosition in
                ScrollView(.horizontal) {
                    
                    // Create a ZStack with an HStack overlaying the chart.
                    // The HStack consists of invisible items that conform to the
                    // identifible protocol to provide positions for programmatic
                    // scrolling to the named location.
                    ZStack {
                        // Create an invisible rectangle for each x axis data point
                        // in the chart.
                        HStack(spacing: 0) {
                            ForEach(chartData) { item in
                                Rectangle()
                                    .fill(.clear)

                                    // Setting maxWidth to .infinity here, combined
                                    // with spacing:0 above, makes the rectangles
                                    // expand to fill the frame specified for the
                                    // chart below.
                                    .frame(maxWidth: .infinity, maxHeight: 0)

                                    // Here, set the rectangle's id to match the
                                    // charted data.
                                    .id(item.id)
                            }
                        }
                        
                        Chart(chartData) {
                            BarMark(x: .value("Day", $0.day),
                                    y: .value("Amount", $0.value),
                                    width: 20)
                        }
                        .frame(width: CGFloat(items) * itemWidth, height: 300)
                    }
                }
                .padding()
                .onChange(of: scrollSpot, perform: {x in
                    if (!x.isEmpty) {
                        scrollPosition.scrollTo(x)
                        scrollSpot = ""
                    }
                })
            }
            .onAppear(perform: populateChart)
        
            Button("Scroll") {
                if let x = chartData.last?.id {
                    print("Scrolling to item \(x)")
                    scrollSpot = x
                }
            }
            
            Spacer()
        }
    }

    func populateChart() {
        if !chartData.isEmpty { return }
        for i in 0..<items {
            chartData.append(ChartData(day: i, value: (i % 10) + 2))
        }
    }
}

IMHO this should work out of the SwiftUI box. Apple's comments for the initializer say it creates a chart composed of a series of identifiable marks. So... if the marks are identifiable, it is not a stretch to expect ScrollViewReader to work with the chart's marks.

But noooooo!

One would hope this is an oversight on Apple's part since the framework is new, and they will expose ids for chart marks in an upcoming release.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM