如何监听SwiftUI Chart横向滚动偏移并获取中心值?
问题描述
需要实现一个支持横向滚动的SwiftUI Chart组件,用户滚动后可选中中心位置的日期对应的数值,效果如下:
现有示例代码:
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) } } }
核心需求:监听Chart的横向滚动偏移,通过chartOverlay的proxy获取滚动时中心位置对应的数值,或找到其他实现选中中心值的思路。
解决方案
方案1:iOS 17+ 原生滚动位置绑定
iOS 17及以上版本的SwiftUI Chart提供了chartScrollPosition修饰符,可以直接绑定滚动位置,无需手动监听偏移:
- 修改
scrollPosition的类型为可选值:
@State private var scrollPosition: String? = "26\nFr"
- 给Chart添加滚动位置绑定:
.chartScrollPosition(x: $scrollPosition)
- 监听滚动位置变化,更新显示数值:
.onChange(of: scrollPosition) { _, newValue in guard let date = newValue else { return } if let selectedData = data.first(where: { $0.date == date }) { priceText = String(format: "%.1f", selectedData.value) } }
方案2:兼容iOS 16的拖拽监听方案
如果需要适配iOS 16,可以在chartOverlay中添加拖拽手势,通过ChartProxy计算中心位置对应的X轴值:
- 完善
chartOverlay部分:
.chartOverlay { proxy in GeometryReader { geo in Color.clear .gesture( DragGesture(minimumDistance: 0) .onChanged { _ in updateSelectedValue(proxy: proxy, geo: geo) } .onEnded { _ in updateSelectedValue(proxy: proxy, geo: geo) } ) .onAppear { updateSelectedValue(proxy: proxy, geo: geo) } } }
- 添加数值更新方法:
private func updateSelectedValue(proxy: ChartProxy, geo: GeometryProxy) { let centerX = geo.frame(in: .local).midX if let dateValue = proxy.value(atX: centerX) as? String { scrollPosition = dateValue if let selectedData = data.first(where: { $0.date == dateValue }) { priceText = String(format: "%.1f", selectedData.value) } } }
完整兼容版代码
以下代码同时支持iOS 16和iOS 17+,还增加了选中项的高亮效果:
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() .font(.title) Chart(data) { flight in BarMark(x: .value("Date", flight.date), y: .value("Price", flight.value), width: 10.0) .foregroundStyle( Gradient(colors: flight.date == scrollPosition ? [.blue, .green] : [.gray.opacity(0.5)]) ) .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(label == scrollPosition ? .blue : .gray) .font(label == scrollPosition ? .headline : .caption) } }) .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 11) .chartYAxis(.hidden) .chartScrollTargetBehavior(.valueAligned(unit: 1)) // iOS 17+ 滚动位置绑定 .chartScrollPosition(x: $scrollPosition) .onChange(of: scrollPosition) { _, newValue in guard let date = newValue else { return } if let selectedData = data.first(where: { $0.date == date }) { priceText = String(format: "%.1f", selectedData.value) } } // iOS 16 兼容拖拽监听 .chartOverlay { proxy in GeometryReader { geo in Color.clear .gesture( DragGesture(minimumDistance: 0) .onChanged { _ in updateSelectedValue(proxy: proxy, geo: geo) } .onEnded { _ in updateSelectedValue(proxy: proxy, geo: geo) } ) .onAppear { updateSelectedValue(proxy: proxy, geo: geo) } } } } .frame(width: .infinity) } } private func updateSelectedValue(proxy: ChartProxy, geo: GeometryProxy) { let centerX = geo.frame(in: .local).midX if let dateValue = proxy.value(atX: centerX) as? String { scrollPosition = dateValue if let selectedData = data.first(where: { $0.date == dateValue }) { priceText = String(format: "%.1f", selectedData.value) } } } }
内容的提问来源于stack exchange,提问作者ceylanburak
相关产品推荐
相关产品推荐

