Swift Charts: Vectorized and function plots
SwiftUI & UI Frameworks 进阶 20m

Swift Charts 向量化绘图与函数图像

Swift Charts: Vectorized and function plots

2024年6月10日

在 Apple 官方观看视频

一句话判断

Swift Charts 今年加了两把利器:函数绘图(LinePlot/AreaPlot)让数学可视化开箱即用,向量化 Plot API 让大数据集渲染性能直接起飞。

这场 Session 讲了什么

Swift Charts 在 iOS 16 发布时定位是”用 SwiftUI 的方式画数据图表”。今年的更新把它推向了两个新方向。

第一个方向是跳出”数据点”的框架,直接绘制数学函数。LinePlotAreaPlot 两个新 API 接受一个闭包,输入 x 返回 y,就能画出函数曲线。支持正态分布、参数方程、分段函数,Audio Graph 也能自动为函数图生成无障碍音频描述。

第二个方向是性能优化。新增的向量化 Plot API(PointPlot、RectanglePlot 等)用 KeyPath 一次性处理整个数据集合,不再需要 ForEach 逐个遍历。对于包含数千甚至数万个点的散点图、热力图,渲染效率提升明显。

两个方向看似不同,底层是同一个设计理念:把”整体”当作一个实体来处理,而不是逐个元素的循环。

值得深挖的点

函数绘图:不只是画曲线,是数学可视化的一整套方案

LinePlot 看起来简单——传一个 (Double) -> Double 的闭包就完事了。但它背后的细节值得说清楚。

Swift Charts 会自动对函数进行采样来推断显示范围(domain),你不需要手动指定 x/y 的边界。但你完全可以覆盖这个行为,通过 .scaleX.scaleY 精确控制显示区间。更进一步,AreaPlot 可以限制自身的采样域(sampling domain),只绘制函数的某一段。

AreaPlot 还支持”两函数之间填充”——返回 (yStart, yEnd) 元组就行。这在展示置信区间、误差范围时非常实用。

参数方程用同一个 LinePlot API,但返回 (x, y) 而不是单个 y。分段函数在无定义的区间返回 .nan,Swift Charts 会自动断开。这些设计都说明 Apple 在 API 表面之下做了不少边界处理。

向量化 Plot API:KeyPath 驱动的批量渲染

Mark API(BarMark、PointMark 等)灵活但慢——每个数据点都要单独创建一个 Mark,各自应用修饰符。对于几千个点的散点图,这个开销不可忽视。

向量化 Plot API 换了一个思路:你给一个集合,用 KeyPath 指定 x、y、颜色、大小等属性,Swift Charts 在内部批量处理。Session 明确建议:把 computed property 转成 stored property,这样 Swift Charts 可以用固定内存偏移量访问数据,避免为每个点调用 getter。

性能建议还包括:如果你的数据有几种不同的样式分组,先按样式排序再传入。Swift Charts 内部需要为每次样式切换付出开销,排序可以减少切换次数。

选择标准很清晰:少量数据点 + 需要单独定制样式 → Mark API;大量数据点 + 整体统一样式 → 向量化 Plot API。

代码片段

绘制正态分布曲线叠加在直方图上

Chart {
    ForEach(histogramData) { point in
        BarMark(x: .value("密度", point.density), y: .value("概率", point.probability))
    }
    // 函数绘图:直接画正态分布曲线
    LinePlot(x: "密度", y: "概率") { x in
        normalDistribution(x: x, mean: mean, std: std)
    }
    .foregroundStyle(.red)
}

场景:在数据直方图上叠加理论分布曲线进行对比。坑:LinePlot 和 BarMark 默认颜色可能相同,记得用 foregroundStyle 区分。

向量化散点图

// 数据结构用 stored property 而非 computed property
struct DataPoint: Identifiable {
    let id: UUID
    let x: Double    // 预计算的投影坐标
    let y: Double    // 预计算的投影坐标
    let capacity: Double
    let axisType: AxisType
}

Chart(dataPoints) { _ in
    PointPlot(x: .value("经度", \.x), y: .value("纬度", \.y))
        .symbolSize(.value("容量", \.capacity))
        .foregroundStyle(.value("类型", \.axisType))
}

场景:数千个太阳能面板安装点在地图上的散点可视化。坑:一定要用 stored property 传 KeyPath,computed property 会严重拖慢渲染。

参数方程绘图

Chart {
    LinePlot(x: "x", y: "y") { t in
        let x = cos(t) * (1 + 0.5 * cos(5 * t))
        let y = sin(t) * (1 + 0.5 * cos(5 * t))
        return (x, y)
    }
}

场景:绘制花形参数曲线等数学图案。坑:确保 t 的范围覆盖完整曲线,否则图形会不完整。

最佳实践

已有项目迁移:如果你用 ForEach + MarkMark 画大数据图表(超过 100 个点),优先迁移到向量化 Plot API。改动不大——主要是把 ForEach 循环换成 Plot 的 KeyPath 语法,同时确保数据模型的属性是 stored property。函数绘图是新能力,可以在需要叠加理论曲线或数学可视化时引入。

新项目起步:从第一天就建立”数据规模决定 API 选择”的意识。100 个点以内用 Mark API 没问题,超过这个量级就考虑向量化 Plot。如果你的 App 涉及任何科学计算或数据分析,函数绘图能力值得立刻上手。

还有什么值得关注

  • 函数图支持 Audio Graph,VoiceOver 用户可以用音频感知函数曲线的走势——无障碍支持做得比大多数第三方图表库都好。
  • AreaPlot 的 (yStart, yEnd) 元组返回方式让”两条曲线之间填充”变得极其简单,不用再手动构造上下界数据。
  • 所有 Mark 类型都有对应的向量化 Plot 变体(RectanglePlot、LinePlot 等),不只是 PointPlot。
WWDC 2024