如何为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) } }
关键实现说明
- 状态管理:添加
@State private var selectedData存储当前选中的数据点,驱动UI更新 - 交互手势:通过
.chartOverlay在图表上层添加拖拽手势,监听触摸位置 - 坐标转换:利用
proxy.value(atX:)将屏幕触摸位置转换为图表的时间轴值 - 数据匹配:通过计算时间差找到最接近触摸点的数据
- 视觉反馈:选中时显示红色标记点,同时在顶部展示格式化的时间和收盘价
- 手势结束处理:拖拽结束后清空选中状态,恢复初始显示
内容的提问来源于stack exchange,提问作者user20166709
相关产品推荐
相关产品推荐

