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

如何扩展Swift Charts的手势/点击响应区域?

扩展Swift Charts中chartXSelection等交互API的手势响应区域

问题描述

在使用iOS 17引入的chartXSelection(value:)、chartXSelection(range:)、chartYSelection(value:)、chartYSelection(range:)、chartAngleSelection(value:)等便捷图表交互API时,默认仅在绘图容器框架(Plot Container Frame)(即坐标轴之间不含轴本身的可见区域)内响应手势。需要让这些交互在更大的区域(比如示例中Chart的绿色padding区域)内也能触发相同的选择效果。

解决方案

利用chartOverlay添加覆盖目标区域的手势识别,通过ChartProxy将点击坐标转换为图表对应轴的数值,手动更新绑定的选中值,同时保留官方API的原有行为。

修改后的完整代码

private struct PlotPoint {
    let x: Double
    let y: Double
}

private struct PlotSeries: Identifiable {
    let id: Int
    let name: String
    let points: [PlotPoint]
}

private func chartPoints(height: Double) -> [PlotPoint] {
    stride(from: 0.0, through: 1.0, by: 0.05)
        .map { y in PlotPoint(x: y*y, y: y*height) }
}

private let data: [PlotSeries] = [
    PlotSeries(id: 0, name: "Series A", points: chartPoints(height: 1.0)),
    PlotSeries(id: 1, name: "Series B", points: chartPoints(height: 0.5))
]

struct ExpandTap: View {
    @State private var selectedX: Double?
    
    var body: some View {
        VStack {
            Chart {
                if let selectedX {
                    BarMark(xStart: .value("x", selectedX - 0.025), xEnd: .value("x", selectedX + 0.025))
                        .foregroundStyle(.yellow.opacity(0.5))
                }
                ForEach(data) { series in
                    let plot = LinePlot( series.points, x: .value("x", \.x), y: .value("y", \.y))
                    plot
                        .symbol(by: .value("Series", series.name))
                        .foregroundStyle(by: .value("Series", series.name))
                }
            }
            .chartXScale(domain: [0, 1])
            .chartXAxis {
                AxisMarks() { value in
                    AxisGridLine()
                    AxisTick()
                    AxisValueLabel(orientation: .vertical, verticalSpacing: 20)
                }
            }
            // 保留官方API处理绘图容器内的点击
            .chartXSelection(value: $selectedX)
            // 添加覆盖扩展区域的手势处理
            .chartOverlay { proxy in
                GeometryReader { geo in
                    Color.clear
                        .contentShape(Rectangle()) // 确保整个区域可响应手势
                        .onTapGesture { location in
                            guard let plotFrame = proxy.plotContainerFrame else { return }
                            // 限制仅响应Chart视图内的点击(绿色区域+绘图容器),排除外部灰色padding
                            let chartLocalFrame = geo.frame(in: .local)
                            guard chartLocalFrame.contains(location) else { return }
                            
                            // 将点击位置转换为X轴对应数值
                            if let xValue = proxy.value(forXPosition: location.x) as? Double {
                                selectedX = xValue
                            }
                        }
                }
            }
            .padding(.leading, 100)
            .padding(.top, 25)
            .padding(.trailing, 50)
            .background(.green.opacity(0.2))
            .background(.white)
            
            .padding(50)
            .background(.gray.opacity(0.4))
            .padding()
            .padding(.trailing, 100)

            Text("selectedX is: \(selectedX?.formatted() ?? "--")")
        }
    }
}

关键细节说明

  1. chartOverlay的作用:chartOverlay会渲染在Chart视图的最上层,我们用透明的Color作为手势载体,配合contentShape(Rectangle())确保整个Chart视图区域(包括绿色padding)都能接收点击事件。
  2. 坐标转换:通过ChartProxy的value(forXPosition:)方法,将视图坐标转换为图表X轴的数值,这和官方chartXSelection的转换逻辑完全一致,保证行为统一。
  3. 区域限制:通过GeometryReader获取Chart视图的本地帧,判断点击位置是否在该帧内,避免响应外部灰色padding区域的点击。
  4. 通用性:如果需要扩展chartYSelection的响应区域,只需将value(forXPosition:)替换为value(forYPosition: location.y),同理可适配其他chart*Selection系列API。

内容的提问来源于stack exchange,提问作者Curious Jorge

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 21:09:50