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

如何为Swift LineMark图表添加逐帧绘制动画?

实现SwiftUI Chart折线逐步绘制动画

你的问题核心在于:SwiftUI Chart的LineMark是基于完整数据集一次性生成折线路径的,单纯给Chart加animation修饰符或者修改无关的状态变量(比如你用的startAnimation),不会触发路径的逐步绘制动画——因为没有改变影响折线形状的核心数据源或进度参数。

下面是可行的解决方案,通过动态截断数据源配合动画来实现折线逐步绘制的效果:

修改后的完整代码

struct ClosingValuesChart: View {
    @State private var selectedDate: Date? = nil
    @State private var selectedClose: Float? = nil
    @State private var xAxisLabels: [Date] = []
    // 新增:控制绘制进度的状态变量,0=未开始,1=绘制完成
    @State private var drawProgress: CGFloat = 0
    var closingValues: [TradingDayClose] = []
    var heading: String = ""
    
    init(fundName: String, numYears: Int, closingValues: [TradingDayClose]) {
        self.heading = fundName + String(" - \(numYears) Year")
        self.closingValues = closingValues
    }
    
    var body: some View {
        GroupBox (heading) {
            let xMin = closingValues.first?.timeStamp
            let xMax = closingValues.last?.timeStamp
            let yMin = closingValues.map { $0.close }.min()!
            let yMax = closingValues.map { $0.close }.max()!
            let xAxisLabels: [Date] = GetXAxisLabels(xMin: xMin!, xMax: xMax!)
            
            var yAxisLabels: [Float] {
                stride(from: yMin, to: yMax + ((yMax - yMin)/7), by: (yMax - yMin) / 7).map { $0 }
            }
            
            // 根据绘制进度切片数据源:只取前N个数据点
            let animatedClosingValues = Array(closingValues.prefix(Int(drawProgress * CGFloat(closingValues.count))))
            
            Chart {
                ForEach(animatedClosingValues) { value in
                    LineMark(
                        x: .value("Time", value.timeStamp!),
                        y: .value("Closing Value", value.close)
                    )
                    .foregroundStyle(Color.blue)
                    .lineStyle(StrokeStyle(lineWidth: 1.25))
                }
            }
            .chartXScale(domain: xMin!...xMax!)
            .chartXAxisLabel(position: .bottom, alignment: .center, spacing: 25) {
                Text("Date")
                    .textFormatting(fontSize: 14)
            }
            .chartXAxis {
                AxisMarks(position: .bottom, values: xAxisLabels) { value in
                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1))
                    AxisValueLabel(anchor: .top) {
                        if value.as(Date.self) != nil {
                            Text("")
                        }
                    }
                }
            }
            .chartYScale(domain: yMin...yMax)
            .chartYAxisLabel(position: .leading, alignment: .center, spacing: 25) {
                Text("Closing Value")
                    .font(Font.custom("Arial", size: 14))
                    .foregroundColor(.black)
            }
            .chartYAxis {
                AxisMarks(position: .leading, values: yAxisLabels) { value in
                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1))
                    AxisValueLabel() {
                        if let labelValue = value.as(Double.self) {
                            Text(String(format: "$ %.2f", labelValue))
                                .textFormatting(fontSize: 12)
                        }
                    }
                }
            }
            .chartOverlay { proxy in
                GeometryReader { geometry in
                    ChartOverlayRectangle(selectedDate: $selectedDate, selectedClose: $selectedClose, proxy: proxy, geometry: geometry, xMin: xMin!, xMax: xMax!, closingValues: closingValues)
                }
            }
            .overlay {
                XAxisDates(dateLabels: xAxisLabels)
            }
            .overlay {
                DateAndClosingValue(selectedDate: selectedDate ?? Date(), selectedClose: selectedClose ?? 0.0)
            }
        }
        .groupBoxStyle(ChartGroupBoxStyle())
        .onAppear {
            // 触发动画:用withAnimation控制进度从0到1,设置动画时长(比如3秒)
            withAnimation(.linear(duration: 3)) {
                drawProgress = 1
            }
        }
    }
}

关键改动说明

  • 新增drawProgress状态变量:用来控制折线绘制的完成度,范围从0到1。
  • 生成animatedClosingValues:根据当前进度对原始数据源进行切片,只保留需要绘制的前N个数据点。
  • 在onAppear中用withAnimation驱动进度变化:设置动画时长(示例中是3秒),让drawProgress从0平滑过渡到1,触发数据源切片的动态更新,从而实现折线逐步延伸的效果。

可选优化:平滑的路径过渡

如果觉得切片方式的过渡不够平滑,可以尝试给LineMark添加transition(.opacity),或者使用trim修饰符(需要自定义路径绘制,适合更复杂的动画需求),但上面的切片方案是最适配SwiftUI Chart组件的轻量实现。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 18:54:57