You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何监听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修饰符,可以直接绑定滚动位置,无需手动监听偏移:

  1. 修改scrollPosition的类型为可选值:
@State private var scrollPosition: String? = "26\nFr"
  1. 给Chart添加滚动位置绑定:
.chartScrollPosition(x: $scrollPosition)
  1. 监听滚动位置变化,更新显示数值:
.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轴值:

  1. 完善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)
            }
    }
}
  1. 添加数值更新方法:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 10:50:59