Swift Charts 进阶指南
Swift Charts: Raise the bar
2022年6月6日
一句话判断
Swift Charts 在 iOS 16/macOS 13 中补齐了自定义能力——mark 组合、axis 精细控制、ChartProxy 交互式选区,让你终于能在不回退到 Core Plot 的情况下做出产品级的图表。
这场 Session 讲了什么
这场 Session 是去年 Swift Charts 介绍(WWDC22 “Hello Swift Charts”)的进阶篇。重点不是”怎么画一个柱状图”,而是”怎么画出你真正想要的那种图表”。
Mark 类型。 Swift Charts 提供了六种基本 mark:BarMark、LineMark、AreaMark、PointMark、RuleMark 和 RectangleMark。每种 mark 可以通过属性来定制——比如 BarMark 的 stacked() 把多个柱子堆叠,positioned(by:) 控制柱子是靠左还是靠右。
Mark 组合。 这是今年最重要的能力。你可以在同一个 Chart {} 里放多个不同类型的 mark,它们会叠加显示。比如柱状图上叠加折线图、面积图上叠加散点图。组合的关键是确保所有 mark 使用相同的数据映射(x: 和 y: 参数),这样它们才能对齐。
数据类型。 Swift Charts 把数据分成三类:quantitative(数值型,如温度、价格)、nominal(分类型,如城市名、产品名)、temporal(时间型,如日期)。你在 mark 的 x: 和 y: 参数里传入不同类型的数据,Swift Charts 会自动选择合适的 axis 和 scale。比如 x 轴传入 Date 类型,系统会用时间轴;传入 String 类型,系统会用分类轴。
Scale 和 Axis 自定义。 chartXScale() 和 chartYScale() 可以控制轴的范围和类型。chartXAxis() 和 chartYAxis() 可以自定义轴的标签、网格线和刻度。你可以用 AxisMarks() 来精细控制每一个刻度的样式——字体大小、前景色、格式化字符串等。
Legend 配置。 chartLegend() modifier 可以控制图例的位置(顶部、底部、前导侧、尾部)或者完全隐藏。如果你用 foregroundStyle(by:) 来区分数据系列,Swift Charts 会自动生成图例。
Plot Area 样式。 chartPlotStyle() 让你可以自定义绘图区域的背景色、圆角、边框等。这看起来是个小功能,但在实际产品中非常重要——默认的白色背景通常需要调整才能匹配 App 的整体设计。
ChartProxy 和交互。 ChartProxy 是一个新类型,让你可以读取图表的布局信息。配合 chartOverlay() modifier,你可以在图表上叠加一个 SwiftUI gesture recognizer,实现”手指滑动显示对应数据点”这种交互效果。这是去年 Swift Charts 缺失的重要能力。
值得深挖的点
ChartProxy 的交互实现。 ChartProxy 提供了 value(atX:) 和 value(atY:) 方法,可以把屏幕坐标转换回数据值。你可以用这个能力实现”brushing”——用户在图表上滑动时,显示一个竖线和对应的数据标签。实现方式是在 chartOverlay 里放一个自定义 SwiftUI view,用 GeometryReader 获取触摸位置,然后用 ChartProxy 转换成数据值。
Mark 组合的性能。 多个 mark 叠加时,Swift Charts 会尝试合并渲染路径。但如果你的数据量很大(比如几万个数据点),同时用 AreaMark + LineMark + PointMark 可能会有性能问题。建议只在需要时使用 PointMark——如果数据点很密集,散点可能看不清,不如只用 LineMark。
代码片段
组合多个 mark 创建复合图表:
import SwiftUICharts
import Charts
struct SalesChart: View {
let data: [SalesData]
var body: some View {
Chart(data) { point in
// 面积图作为背景
AreaMark(
x: .value("Month", point.month),
y: .value("Revenue", point.revenue)
)
.foregroundStyle(.blue.opacity(0.1))
// 折线图
LineMark(
x: .value("Month", point.month),
y: .value("Revenue", point.revenue)
)
.foregroundStyle(.blue)
.lineStyle(StrokeStyle(lineWidth: 2))
// 数据点
PointMark(
x: .value("Month", point.month),
y: .value("Revenue", point.revenue)
)
.foregroundStyle(.blue)
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
}
.chartYAxis {
AxisMarks(position: .leading) { value in
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
.foregroundStyle(.gray.opacity(0.3))
AxisValueLabel {
if let val = value.as(Double.self) {
Text("$\(Int(val / 1000))K")
.font(.caption)
}
}
}
}
}
}
使用 ChartProxy 实现交互式选区:
Chart(data) { point in
BarMark(
x: .value("Category", point.category),
y: .value("Value", point.value)
)
.foregroundStyle(point.category == selectedCategory ? .blue : .gray)
}
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle()
.fill(.clear)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
let location = value.location
// 把屏幕坐标转换为数据值
if let category: String = proxy.value(
atX: location.x
) {
selectedCategory = category
}
}
)
}
}
最佳实践
定义数据模型时用 Plottable 协议。Swift Charts 的 x: 和 y: 参数接受任何 Plottable 类型。Date、String、Double、Int 都已经自动遵循 Plottable。如果你的数据类型更复杂(比如自定义的 Measurement 类型),可以扩展它遵循 Plottable。
组合 mark 时保持数据映射一致。所有叠加的 mark 应该使用相同的 x: 和 y: 数据字段,否则它们不会对齐。如果你需要画不同 y 轴的数据(比如温度和降雨量),用双 y 轴而不是在同一个 axis 上混用不同单位。
chartOverlay 里的手势处理要考虑边界情况。ChartProxy.value(atX:) 可能返回 nil(手指移出图表区域),你的代码需要处理这种情况。另外,在竖屏和横屏切换时图表的尺寸会变化,手势逻辑不应该硬编码任何像素值。
还有什么值得关注
RectangleMark可以用来画热力图(heatmap)——把 x 和 y 都映射到分类数据,用foregroundStyle映射颜色。RuleMark适合画参考线,比如平均值、目标值。配合.lineStyle()可以设置虚线样式。- SwiftUI 的 animation modifier 可以直接应用到 Chart 上,数据变化时图表会自动动画过渡。
chartLegend(_:)支持.visible和.hidden控制,也支持.bottom、.leading等位置参数。如果你需要完全自定义图例样式,可以隐藏默认图例,自己用 SwiftUI 画。