如何为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
相关产品推荐
相关产品推荐

