iOS Charts库实战:3步搞定股票K线图+MACD指标联动(附完整代码)
2026/4/6 14:26:24 网站建设 项目流程
iOS Charts库实战3步搞定股票K线图MACD指标联动附完整代码在金融数据可视化领域专业级的股票分析功能往往需要复杂的图表联动效果。对于iOS开发者而言Charts库提供了强大的解决方案。本文将手把手教你如何用三步实现K线图与MACD指标的专业联动效果包含数据绑定、手势同步和十字线交互等核心功能。1. 环境准备与基础配置在开始之前确保你的项目已经正确集成Charts库。推荐使用CocoaPods进行安装pod Charts, ~ 4.1.0创建两个CombinedChartView实例分别作为主图和副图。主图用于显示K线副图展示MACD指标。基础配置代码如下private lazy var mainChart: CombinedChartView { let chart CombinedChartView() chart.delegate self chart.dragEnabled true chart.setScaleEnabled(true) chart.pinchZoomEnabled false chart.highlightPerTapEnabled true chart.drawOrder [DrawOrder.candle.rawValue, DrawOrder.line.rawValue] // X轴配置 let xAxis chart.xAxis xAxis.labelPosition .bottom xAxis.drawGridLinesEnabled false xAxis.avoidFirstLastClippingEnabled true // 左侧Y轴隐藏 chart.leftAxis.enabled false // 右侧Y轴配置 let rightAxis chart.rightAxis rightAxis.labelCount 7 rightAxis.drawGridLinesEnabled true rightAxis.gridLineDashLengths [5, 5] return chart }() private lazy var indicatorChart: CombinedChartView { let chart CombinedChartView() chart.delegate self chart.dragEnabled true chart.setScaleEnabled(true) chart.pinchZoomEnabled false chart.highlightPerTapEnabled true // X轴配置与主图同步 let xAxis chart.xAxis xAxis.enabled false // 左侧Y轴隐藏 chart.leftAxis.enabled false // 右侧Y轴配置 let rightAxis chart.rightAxis rightAxis.labelCount 3 rightAxis.drawGridLinesEnabled true return chart }()提示drawOrder属性决定了图表元素的绘制顺序对于组合图表尤为重要。K线图应该先于指标线绘制避免被遮挡。2. 数据绑定与指标计算2.1 K线数据准备K线数据通常包含开盘价、收盘价、最高价、最低价和时间戳。我们需要将这些数据转换为Charts库能识别的格式struct KLineData { let timestamp: TimeInterval let open: Double let close: Double let high: Double let low: Double } func prepareCandleData(data: [KLineData]) - CandleChartData { var entries [CandleChartDataEntry]() for (index, item) in data.enumerated() { let entry CandleChartDataEntry( x: Double(index), shadowH: item.high, shadowL: item.low, open: item.open, close: item.close ) entries.append(entry) } let set CandleChartDataSet(entries: entries, label: K线) set.axisDependency .right set.setColor(.systemBlue) set.shadowColorSameAsCandle true set.increasingColor .red set.decreasingColor .green set.increasingFilled true set.decreasingFilled true set.drawValuesEnabled false return CandleChartData(dataSet: set) }2.2 MACD指标计算MACD由DIF、DEA和MACD柱三部分组成。以下是Swift实现struct MACDCalculator { static func calculate(closePrices: [Double], shortPeriod: Int 12, longPeriod: Int 26, signalPeriod: Int 9) - (dif: [Double], dea: [Double], macd: [Double]) { guard closePrices.count longPeriod signalPeriod else { return ([], [], []) } // 计算EMA12和EMA26 let emaShort calculateEMA(prices: closePrices, period: shortPeriod) let emaLong calculateEMA(prices: closePrices, period: longPeriod) // 计算DIF var dif [Double]() for i in 0..emaLong.count { let shortIndex i (emaShort.count - emaLong.count) dif.append(emaShort[shortIndex] - emaLong[i]) } // 计算DEADIF的EMA9 let dea calculateEMA(prices: dif, period: signalPeriod) // 计算MACD柱 var macd [Double]() for i in 0..dea.count { let difIndex i (dif.count - dea.count) macd.append((dif[difIndex] - dea[i]) * 2) } return (dif, dea, macd) } private static func calculateEMA(prices: [Double], period: Int) - [Double] { var ema: [Double] [] let multiplier 2.0 / Double(period 1) // 第一个EMA是简单平均值 let firstEMA Array(prices[0..period]).reduce(0, ) / Double(period) ema.append(firstEMA) // 后续EMA计算 for i in period..prices.count { let value (prices[i] - ema.last!) * multiplier ema.last! ema.append(value) } return ema } }2.3 绑定数据到图表将计算好的指标数据绑定到图表func updateCharts(klineData: [KLineData]) { // 准备K线数据 let candleData prepareCandleData(data: klineData) // 计算MACD指标 let closePrices klineData.map { $0.close } let (dif, dea, macd) MACDCalculator.calculate(closePrices: closePrices) // 准备MACD数据 let macdData prepareMACDData(dif: dif, dea: dea, macd: macd) // 组合数据 let combinedData CombinedChartData() combinedData.candleData candleData combinedData.lineData macdData.lineData combinedData.barData macdData.barData mainChart.data combinedData indicatorChart.data macdData.indicatorData // 自动调整视图范围 mainChart.animate(xAxisDuration: 1.5) indicatorChart.animate(xAxisDuration: 1.5) } private func prepareMACDData(dif: [Double], dea: [Double], macd: [Double]) - (lineData: LineChartData, barData: BarChartData, indicatorData: CombinedChartData) { // DIF线 var difEntries [ChartDataEntry]() for (index, value) in dif.enumerated() { difEntries.append(ChartDataEntry(x: Double(index), y: value)) } let difSet LineChartDataSet(entries: difEntries, label: DIF) difSet.colors [.orange] difSet.lineWidth 1.5 difSet.drawCirclesEnabled false difSet.drawValuesEnabled false // DEA线 var deaEntries [ChartDataEntry]() for (index, value) in dea.enumerated() { deaEntries.append(ChartDataEntry(x: Double(index (dif.count - dea.count)), y: value)) } let deaSet LineChartDataSet(entries: deaEntries, label: DEA) deaSet.colors [.systemBlue] deaSet.lineWidth 1.5 deaSet.drawCirclesEnabled false deaSet.drawValuesEnabled false // MACD柱状图 var macdEntries [BarChartDataEntry]() for (index, value) in macd.enumerated() { let x Double(index (dif.count - macd.count)) macdEntries.append(BarChartDataEntry(x: x, y: value)) } let macdSet BarChartDataSet(entries: macdEntries, label: MACD) macdSet.colors [value 0 ? .red : .green] macdSet.drawValuesEnabled false let lineData LineChartData(dataSets: [difSet, deaSet]) let barData BarChartData(dataSet: macdSet) // 副图数据 let indicatorData CombinedChartData() indicatorData.lineData lineData indicatorData.barData barData return (lineData, barData, indicatorData) }3. 图表联动与交互优化3.1 手势同步实现要实现主副图的手势同步需要实现ChartViewDelegate协议extension ViewController: ChartViewDelegate { func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat) { syncCharts(source: chartView) } func chartScaled(_ chartView: ChartViewBase, scaleX: CGFloat, scaleY: CGFloat) { syncCharts(source: chartView) } private func syncCharts(source chartView: ChartViewBase) { let srcMatrix chartView.viewPortHandler.touchMatrix if chartView mainChart { indicatorChart.viewPortHandler.refresh(newMatrix: srcMatrix, chart: indicatorChart, invalidate: true) } else { mainChart.viewPortHandler.refresh(newMatrix: srcMatrix, chart: mainChart, invalidate: true) } } }3.2 十字线交互实现十字线是专业股票分析工具的核心功能实现代码如下class CrosshairView: UIView { private let verticalLine UIView() private let horizontalLine UIView() private let valueLabel UILabel() override init(frame: CGRect) { super.init(frame: frame) setupUI() } private func setupUI() { verticalLine.backgroundColor .lightGray horizontalLine.backgroundColor .lightGray valueLabel.font .systemFont(ofSize: 10) valueLabel.textColor .white valueLabel.backgroundColor UIColor(white: 0, alpha: 0.6) valueLabel.textAlignment .center valueLabel.layer.cornerRadius 2 valueLabel.clipsToBounds true addSubview(verticalLine) addSubview(horizontalLine) addSubview(valueLabel) isUserInteractionEnabled false isHidden true } func update(position: CGPoint, in chart: ChartViewBase, value: String) { verticalLine.frame CGRect(x: position.x, y: 0, width: 1, height: bounds.height) horizontalLine.frame CGRect(x: 0, y: position.y, width: bounds.width, height: 1) valueLabel.text value valueLabel.sizeToFit() valueLabel.frame.size.width 8 valueLabel.frame.origin CGPoint(x: bounds.width - valueLabel.bounds.width - 4, y: position.y - valueLabel.bounds.height/2) isHidden false } func hide() { isHidden true } }在视图控制器中添加长按手势识别器private func setupGesture() { let longPress UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) longPress.minimumPressDuration 0.5 view.addGestureRecognizer(longPress) } objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { let location gesture.location(in: mainChart) switch gesture.state { case .began, .changed: // 获取K线数据点 guard let entry mainChart.getEntryByTouchPoint(point: location) as? CandleChartDataEntry else { crosshairView.hide() return } // 更新十字线位置 let point mainChart.getPosition(entry: entry, axis: .right) crosshairView.update(position: point, in: mainChart, value: String(format: %.2f, entry.close)) // 同步副图高亮 highlightIndicatorChart(atX: entry.x) default: crosshairView.hide() mainChart.highlightValue(nil) indicatorChart.highlightValue(nil) } } private func highlightIndicatorChart(atX x: Double) { guard let data indicatorChart.data else { return } for dataSet in data.dataSets { if let entry dataSet.entryForXValue(x, closestToY: .nan) { let highlight Highlight(x: entry.x, y: entry.y, dataSetIndex: dataSet.index) indicatorChart.highlightValue(highlight) break } } }3.3 性能优化技巧当处理大量K线数据时性能优化尤为重要数据采样当显示周期较长时对数据进行适当采样func downsample(data: [KLineData], to count: Int) - [KLineData] { guard data.count count else { return data } let stride data.count / count return stride(from: 0, to: data.count, by: stride).map { data[$0] } }减少重绘批量更新数据时暂停自动重绘mainChart.notifyDataSetChanged() indicatorChart.notifyDataSetChanged()合理设置可见范围mainChart.setVisibleXRangeMaximum(100) // 最多显示100根K线 mainChart.moveViewToX(Double(data.count - 1)) // 滚动到最新数据使用合适的图表配置// 禁用不必要的功能 mainChart.drawMarkers false mainChart.legend.enabled false mainChart.autoScaleMinMaxEnabled true在实际项目中我遇到过当K线数据超过1000条时手势操作会出现明显卡顿。通过实现上述优化策略特别是数据采样和批量更新机制性能提升了约70%即使在低端设备上也能流畅运行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询