Highlight
Swift Charts 在基本图表之外提供了丰富的 Mark 组合、自定义坐标轴样式、plot area 背景和手势交互能力,让开发者可以构建从简单到复杂的完整数据可视化体验。
核心内容
给 App 加上一个柱状图或折线图并不难,Hello Swift Charts 的 session 已经演示了基本用法。但真正上线一个数据看板时,你会遇到一堆问题:如何在同一张图里组合多种视觉元素?如何让 X 轴标签按季度分组?如何让用户用手指拖拽选择数据范围?
这些需求在之前需要自己写 Core Graphics 或引入第三方库。Swift Charts 在基础 Mark 之上提供了一套组合与定制机制,让复杂图表的实现成本大幅降低。
这个 session 从零开始,用同一个煎饼销售数据集,逐步叠加复杂度——从单 Mark 到多 Mark 叠加,从默认样式到完全自定义坐标轴,再到拖拽交互。整个过程展示了 Swift Charts 从”能用”到”好用”的完整路径。
详细内容
组合多种 Mark 表达丰富数据
(3:48)横向柱状图是展示排名数据的经典选择。交换 x 和 y 的值即可得到横向布局:
Chart(data, id: \.name) {
BarMark(
x: .value("Sales", $0.sales),
y: .value("Name", $0.name)
)
.foregroundStyle(.pink)
.accessibilityLabel($0.name)
.accessibilityValue("\($0.sales) sold")
}
关键点:
BarMark的 x 放数值、y 放类别,自动渲染为横向柱状图.foregroundStyle(.pink)统一设置颜色.accessibilityLabel和.accessibilityValue为 VoiceOver 提供定制描述
(5:12)对于时序数据,用 LineMark 替换 BarMark 只需改一个类型名:
Chart {
ForEach(dailySales, id: \.day) {
BarMark(
x: .value("Day", $0.day, unit: .day),
y: .value("Sales", $0.sales)
)
}
}
关键点:
unit: .day告诉框架数据的时间粒度,自动生成合适的轴标签- 将
BarMark改为LineMark即可切换图表类型,数据层不需要任何改动
(6:16)多城市对比时,LineMark 配合 .foregroundStyle(by:) 自动为不同系列分配颜色:
Chart {
ForEach(seriesData, id: \.city) { series in
ForEach(series.data, id: \.weekday) {
LineMark(
x: .value("Weekday", $0.weekday, unit: .day),
y: .value("Sales", $0.sales)
)
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
.interpolationMethod(.catmullRom)
}
}
关键点:
- 嵌套
ForEach处理多系列数据:外层遍历城市,内层遍历该城市的每日数据 .foregroundStyle(by:)按城市分组着色,自动生成图例.symbol(by:)为不同系列使用不同数据点标记.interpolationMethod(.catmullRom)使用 Catmull-Rom 曲线平滑连线
(7:19)改成 BarMark 进行分组柱状图对比时,关键区别是 .position(by:):
BarMark(
x: .value("Weekday", $0.weekday, unit: .day),
y: .value("Sales", $0.sales)
)
.foregroundStyle(by: .value("City", series.city))
.position(by: .value("City", series.city))
关键点:
.position(by:)让同一 x 位置的柱子按城市并排显示- 不加
.position(by:)时柱子会堆叠在一起 - 从
LineMark切换到BarMark只需改类型名并加上.position(by:)
(8:02)在同一张图中叠加多种 Mark,可以同时展示数据范围和平均值:
Chart {
ForEach(data, id: \.month) {
AreaMark(
x: .value("Month", $0.month, unit: .month),
yStart: .value("Daily Min", $0.dailyMin),
yEnd: .value("Daily Max", $0.dailyMax)
)
.opacity(0.3)
LineMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Daily Average", $0.dailyAverage)
)
}
}
关键点:
AreaMark用yStart和yEnd定义区域范围,展示每月最低到最高销量区间LineMark叠加在区域上方,展示每日平均值趋势.opacity(0.3)让区域半透明,确保折线清晰可见
(8:46)另一种组合方式:BarMark + RectangleMark:
BarMark(
x: .value("Month", $0.month, unit: .month),
yStart: .value("Daily Min", $0.dailyMin),
yEnd: .value("Daily Max", $0.dailyMax),
width: .ratio(0.6)
)
.opacity(0.3)
RectangleMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Daily Average", $0.dailyAverage),
width: .ratio(0.6),
height: 2
)
关键点:
BarMark的width: .ratio(0.6)控制柱子宽度为可用空间的 60%RectangleMark在这里充当”平均值横线”,height: 2让它看起来像一条线- 两种 Mark 共享同一数据,展示不同维度的信息
(9:19)RuleMark 可以在图表中添加参考线和注释:
RuleMark(
y: .value("Average", averageValue)
)
.lineStyle(StrokeStyle(lineWidth: 3))
.annotation(position: .top, alignment: .leading) {
Text("Average: \(averageValue, format: .number)")
.font(.headline)
.foregroundStyle(.blue)
}
关键点:
RuleMark在 y 轴指定值处画一条水平参考线.lineStyle(StrokeStyle(lineWidth: 3))设置线宽.annotation(position: .top, alignment: .leading)在参考线顶部添加文本注释
自定义坐标轴样式
(13:54)通过 chart modifiers 控制图表比例和颜色映射:
Chart {
// ... LineMark 定义 ...
}
.chartYScale(domain: 0 ... 200)
.chartForegroundStyleScale([
"San Francisco": .orange,
"Cupertino": .pink
])
关键点:
.chartYScale(domain:)手动设置 y 轴范围,避免自动缩放导致不同图表间难以比较.chartForegroundStyleScale([:])用字典精确控制每个系列的映射颜色
(15:16)自定义 X 轴刻度和标签格式:
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(
format: .dateTime.month(.narrow)
)
}
}
关键点:
AxisMarks(values: .stride(by: .month))按月份步进生成刻度AxisGridLine()绘制网格线AxisTick()绘制刻度线AxisValueLabel(format:)用.dateTime.month(.narrow)格式化标签为缩写月份(J、A、S 等)
(16:17)在 AxisMarks 闭包中做条件判断,实现季度标记:
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
if value.as(Date.self)!.isFirstMonthOfQuarter {
AxisGridLine().foregroundStyle(.black)
AxisTick().foregroundStyle(.black)
AxisValueLabel(
format: .dateTime.month(.narrow)
)
} else {
AxisGridLine()
}
}
}
关键点:
value.as(Date.self)将轴值转换为 Date 类型- 通过自定义的
isFirstMonthOfQuarter扩展判断是否为季度首月 - 季度首月显示完整标记(网格线、刻度线、标签),其他月份只显示网格线
- 这个模式适用于任何”间隔高亮”的需求
(17:00)自定义 Y 轴:
.chartYAxis {
AxisMarks(preset: .extended, position: .leading)
}
关键点:
preset: .extended扩展轴标记范围position: .leading将 Y 轴放在左侧
自定义绘图区域样式
(18:26)使用 .chartPlotStyle 修改绘图区域的外观:
.chartPlotStyle { plotArea in
plotArea.frame(height: 60 * 6)
.background(.pink.opacity(0.2))
.border(.pink, width: 1)
}
关键点:
plotArea是绘图区域视图的代理,可以直接对其应用 SwiftUI modifier.frame(height: 60 * 6)手动设定高度,每行 60pt 对应 6 条数据- 可以设置背景色、边框等任意 SwiftUI 样式
拖拽选择交互
(20:03)通过 .chartOverlay 添加手势交互,实现刷选(brushing)功能:
struct InteractiveBrushingChart: View {
@State var range: (Date, Date)? = nil
var body: some View {
Chart {
ForEach(data, id: \.day) {
LineMark(
x: .value("Month", $0.day, unit: .day),
y: .value("Sales", $0.sales)
)
.interpolationMethod(.catmullRom)
.symbol(Circle().strokeBorder(lineWidth: 2))
}
if let (start, end) = range {
RectangleMark(
xStart: .value("Selection Start", start),
xEnd: .value("Selection End", end)
)
.foregroundStyle(.gray.opacity(0.2))
}
}
.chartOverlay { proxy in
GeometryReader { nthGeoItem in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(DragGesture()
.onChanged { value in
let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x
let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x
if let dateStart: Date = proxy.value(atX: xStart),
let dateCurrent: Date = proxy.value(atX: xCurrent) {
range = (dateStart, dateCurrent)
}
}
.onEnded { _ in range = nil }
)
}
}
}
}
关键点:
.chartOverlay在图表上方叠加 SwiftUI 视图,支持手势识别proxy.plotAreaFrame获取绘图区域的坐标框架,用于坐标转换proxy.value(atX:)将屏幕 x 坐标转换回图表数据域中的 Date 值GeometryReader用于获取 overlay 视图在屏幕坐标系中的位置RectangleMark用xStart和xEnd渲染选中区域的高亮- 手势结束时
range = nil清除选择状态 .contentShape(Rectangle())确保透明区域也能响应手势
核心启发
数据看板 App 的”区间高亮”功能
- 利用
AreaMark+LineMark叠加展示每日波动范围与平均值趋势,再通过RuleMark添加行业基准线 - 用
.chartOverlay+DragGesture实现拖拽选择时间区间,用RectangleMark渲染选中高亮 - 从
proxy.value(atX:)获取选中的日期范围后,联动下方的统计卡片展示区间汇总数据
运动数据可视化
- 用
BarMark+.position(by:)对比每周不同运动的时长,横向柱状图展示运动排名 - 用
.chartYScale(domain:)固定 y 轴范围,避免不同周之间的视觉误导 - 用
.chartXAxis自定义刻度,只标注每周的第一天
财务趋势图
- 用
LineMark+.interpolationMethod(.catmullRom)绘制平滑的资产走势 - 用
AreaMark的yStart/yEnd展示每月收入/支出范围 - 用
.chartForegroundStyleScale自定义支出/收入的配色方案 - 用
RuleMark标注预算线,超出预算时改变注释颜色
天气数据对比
- 用
LineMark配合.foregroundStyle(by:)展示多个城市的温度曲线 - 用
.symbol(by:)为每个城市分配不同形状的数据点标记 - 用
.chartPlotStyle给绘图区域添加淡蓝色背景,增强”天气”的视觉感知
排行榜动画
- 利用 SwiftUI 的数据驱动特性,在数据变化时配合
.animation()实现排行榜位次变化的平滑过渡 - 用横向
BarMark+.foregroundStyle渐变展示排名,排名越高颜色越深 - 用
.chartPlotStyle设置固定高度,避免数据量变化时图表高度跳动
关联 Session
- Hello Swift Charts — say hello to swift charts — a flexible framework that helps you create charts entirely in swiftui that look and feel right at home on all apple platforms
- Design an effective chart — learn how to design focused, approachable, and accessible charts with clear marks, axes, descriptions, interaction, and color
- What’s new in SwiftUI — join us as we share the latest updates and a glimpse into the future of ui framework design including swift charts
评论
GitHub Issues · utterances