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

如何为SwiftUI金融折线图添加手势以显示收盘价与时间戳

SwiftUI 金融折线图添加交互手势展示数据

需求说明

为现有金融折线图添加交互手势,触发时显示对应数据的收盘价(close)和时间戳(timestamp)。

原实现代码

struct ContentView: View {
    
    let stockData: [StockPriceData]
   
    var body: some View {
        
        ChartView
            .chartYScale(domain: stockData.map {$0.close}.min()!...stockData.map{$0.close}.max()!)
        
            .chartXAxis{
                AxisMarks(format: Date.FormatStyle(date:  .omitted, time: .shortened, timeZone: TimeZone(secondsFromGMT: -20)!))
            }
    }
    
    private var ChartView: some View {
        Chart {
            ForEach(stockData) {
                LineMark(x: .value("time", $0.timestamp),
                         y: .value("price", $0.close))
                    .foregroundStyle(Color.green)
                
                AreaMark(x: .value("time", $0.timestamp),
                         yStart: .value("Min", stockData.map{$0.close}.min()!),
                         yEnd: .value("Max", $0.close)
                ).foregroundStyle(LinearGradient(gradient: Gradient.init(colors: [Color.green, .clear]), startPoint: .top, endPoint: .bottom)).opacity(0.1)
            }
            }.frame(width: UIScreen.main.bounds.width - 10, height: 200)
        
        }
    }

数据模型及示例数据

struct StockPriceData: Identifiable {
    var id = UUID()
    var timestamp: Date
    var open: Double
    var high: Double
    var low: Double
    var close: Double
}

let stockData: [StockPriceData] = [
    StockPriceData(timestamp: Date().addingTimeInterval(1695374220000), open: 0.86552, high: 0.86655, low: 0.8651, close: 0.86642),
    StockPriceData(timestamp: Date().addingTimeInterval(1695376800000), open: 0.86646, high: 0.86686, low: 0.86622, close: 0.86667),
    StockPriceData(timestamp: Date().addingTimeInterval(1695380400000), open: 0.86667, high: 0.86693, low: 0.86613, close: 0.86669),
    StockPriceData(timestamp: Date().addingTimeInterval(1695384000000), open: 0.86669, high: 0.86739, low: 0.86651, close: 0.86671),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695387600000), open: 0.8667, high: 0.86708, low: 0.86644, close: 0.86671),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695391200000), open: 0.86672, high: 0.86858, low: 0.86648, close: 0.86819),
    StockPriceData(timestamp: Date().addingTimeInterval(1695394800000), open: 0.86818, high: 0.86901, low: 0.8679, close: 0.86839),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695398400000), open: 0.86838, high: 0.86957, low: 0.86759, close: 0.8695),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695402000000), open: 0.86949, high: 0.86957, low: 0.86825, close: 0.86877),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695405600000), open: 0.86877, high: 0.86927, low: 0.86864, close: 0.86873),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695409200000), open: 0.86871, high: 0.86913, low: 0.86835, close: 0.86868),
    StockPriceData(timestamp: Date().addingTimeInterval(1695412800000), open: 0.86869, high: 0.86881, low: 0.868, close: 0.86809),
    StockPriceData(timestamp: Date().addingTimeInterval(1695416400000), open: 0.8681, high: 0.86856, low: 0.86802, close: 0.86831),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695420000000), open: 0.86829, high: 0.86841, low: 0.86796, close: 0.86826),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695596460000), open: 0.86686, high: 0.86808, low: 0.86653, close: 0.86781),
    StockPriceData(timestamp: Date().addingTimeInterval(1695600000000), open: 0.86771, high: 0.86825, low: 0.86761, close: 0.86811),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695603600000), open: 0.86811, high: 0.86842, low: 0.86773, close: 0.868),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695607200000), open: 0.86799, high: 0.86877, low: 0.86792, close: 0.86818),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695610800000), open: 0.86818, high: 0.86832, low: 0.86664, close: 0.86672),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695614400000), open: 0.86672, high: 0.8669, low: 0.86604, close: 0.86656),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695618000000), open: 0.86656, high: 0.86675, low: 0.8663, close: 0.86642),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695621600000), open: 0.86642, high: 0.86667, low: 0.86619, close: 0.86642),
    StockPriceData(timestamp: Date().addingTimeInterval( 1695625200000), open: 0.86643, high: 0.86658, low: 0.86529, close: 0.86547),
    StockPriceData(timestamp: Date().addingTimeInterval(1695628800000), open: 0.86546, high: 0.8658, low: 0.86542, close: 0.86567)
]

修改后的完整实现代码

struct ContentView: View {
    let stockData: [StockPriceData]
    @State private var selectedData: StockPriceData?
    
    var body: some View {
        VStack(spacing: 12) {
            // 显示选中的数据信息
            if let data = selectedData {
                VStack(alignment: .center, spacing: 4) {
                    Text(data.timestamp.formatted(
                        date: .omitted,
                        time: .shortened,
                        timeZone: TimeZone(secondsFromGMT: -20)!
                    ))
                    Text("收盘价: \(data.close, specifier: "%.5f")")
                        .font(.headline)
                        .foregroundColor(.green)
                }
                .padding(.vertical, 8)
                .padding(.horizontal, 16)
                .background(Color(.systemGray6))
                .cornerRadius(8)
            }
            
            ChartView
                .chartYScale(domain: stockData.map {$0.close}.min()!...stockData.map{$0.close}.max()!)
                .chartXAxis{
                    AxisMarks(format: Date.FormatStyle(
                        date: .omitted,
                        time: .shortened,
                        timeZone: TimeZone(secondsFromGMT: -20)!
                    ))
                }
                .chartOverlay { proxy in
                    GeometryReader { geometry in
                        Color.clear
                            .contentShape(Rectangle())
                            .gesture(
                                DragGesture(minimumDistance: 0)
                                    .onChanged { value in
                                        let touchLocation = value.location
                                        // 将触摸位置转换为图表的X轴值(时间)
                                        guard let xDate = proxy.value(atX: touchLocation.x) as Date? else {
                                            selectedData = nil
                                            return
                                        }
                                        // 找到最接近触摸时间的数据点
                                        selectedData = stockData.min(by: {
                                            abs($0.timestamp.timeIntervalSince(xDate)) < abs($1.timestamp.timeIntervalSince(xDate))
                                        })
                                    }
                                    .onEnded { _ in
                                        selectedData = nil
                                    }
                            )
                    }
                }
        }
        .padding()
    }
    
    private var ChartView: some View {
        Chart {
            ForEach(stockData) { data in
                LineMark(x: .value("time", data.timestamp),
                         y: .value("price", data.close))
                .foregroundStyle(Color.green)
                
                AreaMark(x: .value("time", data.timestamp),
                         yStart: .value("Min", stockData.map{$0.close}.min()!),
                         yEnd: .value("Max", data.close))
                .foregroundStyle(LinearGradient(gradient: Gradient(colors: [Color.green, .clear]), startPoint: .top, endPoint: .bottom))
                .opacity(0.1)
                
                // 选中时显示红色标记点
                if selectedData?.id == data.id {
                    PointMark(x: .value("time", data.timestamp),
                              y: .value("price", data.close))
                    .foregroundStyle(Color.red)
                    .symbolSize(100)
                }
            }
        }
        .frame(width: UIScreen.main.bounds.width - 20, height: 200)
    }
}

关键实现说明

  1. 状态管理:添加@State private var selectedData存储当前选中的数据点,驱动UI更新
  2. 交互手势:通过.chartOverlay在图表上层添加拖拽手势,监听触摸位置
  3. 坐标转换:利用proxy.value(atX:)将屏幕触摸位置转换为图表的时间轴值
  4. 数据匹配:通过计算时间差找到最接近触摸点的数据
  5. 视觉反馈:选中时显示红色标记点,同时在顶部展示格式化的时间和收盘价
  6. 手势结束处理:拖拽结束后清空选中状态,恢复初始显示

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 00:42:33