使用 Swift Charts 实现、审查或改进数据可视化。适用于构建柱状图、折线图、面积图、散点图、饼图、环形图或 iOS 26 的 3D 图表;添加图表选择、滚动、标注、坐标轴、比例尺、图例或 foregroundStyle 分组;使用 BarPlot、LinePlot、AreaPlot、PointPlot、Chart3D 或 SurfacePlot 绘制函数;或创建热力图、甘特图、分组柱状图、迷你图、阈值线或空间可视化。
Swift Charts
使用 Swift Charts 构建数据可视化,目标平台 iOS 26+。在 Chart 或 Chart3D 内部组合标记(marks),通过视图修饰符配置坐标轴和比例尺,并在数据需要时使用向量化绘图或 3D 绘图。
有关扩展模式、3D 图表、无障碍和主题指南,请参阅 references/charts-patterns.md。
目录
- 工作流程
- 图表容器
- 标记类型
- 坐标轴自定义
- 比例尺配置
- 前景样式与编码
- 选择(iOS 17+)
- 可滚动图表(iOS 17+)
- 标注
- 图例
- 向量化绘图(iOS 18+)
- 3D 图表(iOS 26+)
- 常见错误
- 审查清单
- 参考资料
工作流程
1. 构建新图表
- 将数据定义为
Identifiable结构体,或使用id:键路径。 - 选择标记类型:
BarMark、LineMark、PointMark、AreaMark、RuleMark、RectangleMark、SectorMark或SurfacePlot。 - 将 2D 标记包裹在
Chart中;仅对真正的空间或曲面数据使用Chart3D。 - 编码视觉通道:
.foregroundStyle(by:)、.symbol(by:)、.lineStyle(by:)。 - 使用
.chartXAxis/.chartYAxis配置坐标轴。 - 使用
.chartXScale(domain:)/.chartYScale(domain:)设置比例尺域。 - 根据需要添加选择、滚动或标注。
- 对于 1000+ 个 2D 数据点,使用向量化绘图(
BarPlot、LinePlot等)。 - 渲染具有代表性的空状态、典型状态、密集状态、大文本、高对比度和 VoiceOver 状态;如果存在选择和滚动,则进行测试。
- 如果编码、坐标轴、选择或无障碍检查失败,则恢复数据夹具,修复一层,并在添加装饰之前重新运行相同的矩阵。
2. 审查现有图表代码
在判断标记选择之前,先确定数据语义。然后追踪每个值通过标记、比例尺、坐标轴、样式、选择和无障碍输出的过程;运行与新图表相同的验证矩阵。
图表容器
Chart(sales) { item in
BarMark(x: .value("Month", item.month), y: .value("Revenue", item.revenue))
}
对单个集合使用数据驱动初始化器。对混合标记或多个系列使用内容闭包,并在元素不是 Identifiable 时传递 id:。加载 references/charts-patterns.md 获取完整的混合系列、主题、无障碍和 3D 示例。
标记类型
BarMark (iOS 16+)
// 垂直柱状图
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
// 按类别堆叠(当相同 x 映射到多个柱时自动发生)
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
.foregroundStyle(by: .value("Product", item.product))
// 水平柱状图
BarMark(x: .value("Sales", item.sales), y: .value("Month", item.month))
// 区间柱状图(甘特图)
BarMark(
xStart: .value("Start", item.start),
xEnd: .value("End", item.end),
y: .value("Task", item.task)
)
LineMark (iOS 16+)
// 单条折线
LineMark(x: .value("Date", item.date), y: .value("Price", item.price))
// 通过 foregroundStyle 编码的多系列折线
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
.foregroundStyle(by: .value("City", item.city))
.interpolationMethod(.catmullRom)
// 使用显式 series 参数的多系列折线
LineMark(
x: .value("Date", item.date),
y: .value("Price", item.price),
series: .value("Ticker", item.ticker)
)
PointMark (iOS 16+)
PointMark(x: .value("Height", item.height), y: .value("Weight", item.weight))
.foregroundStyle(by: .value("Species", item.species))
.symbol(by: .value("Species", item.species))
.symbolSize(100)
AreaMark (iOS 16+)
// 堆叠面积图
AreaMark(x: .value("Date", item.date), y: .value("Sales", item.sales))
.foregroundStyle(by: .value("Category", item.category))
// 范围带
AreaMark(
x: .value("Date", item.date),
yStart: .value("Min", item.min),
yEnd: .value("Max", item.max)
)
.opacity(0.3)
RuleMark (iOS 16+)
RuleMark(y: .value("Target", 9000))
.foregroundStyle(.red)
.lineStyle(StrokeStyle(dash: [5, 3]))
.annotation(position: .top, alignment: .leading) {
Text("Target").font(.caption).foregroundStyle(.red)
}
RectangleMark (iOS 16+)
RectangleMark(x: .value("Hour", item.hour), y: .value("Day", item.day))
.foregroundStyle(by: .value("Intensity", item.intensity))
SectorMark (iOS 17+)
使用 SectorMark 表示严格正的值;在饼图或环形图外部过滤、聚合或解释零/负值。
// 饼图
Chart(data, id: \.name) { item in
SectorMark(angle: .value("Sales", item.sales))
.foregroundStyle(by: .value("Category", item.name))
}
// 环形图
Chart(data, id: \.name) { item in
SectorMark(
angle: .value("Sales", item.sales),
innerRadius: .ratio(0.618),
outerRadius: .inset(10),
angularInset: 1
)
.cornerRadius(4)
.foregroundStyle(by: .value("Category", item.name))
}
坐标轴自定义
// 隐藏坐标轴
.chartXAxis(.hidden)
.chartYAxis(.hidden)
// 自定义坐标轴内容
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
}
// 多个 AxisMarks 组合(网格和标签使用不同间隔)
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { _ in AxisGridLine() }
AxisMarks(values: .stride(by: .week)) { _ in
AxisTick()
AxisValueLabel(format: .dateTime.week())
}
}
// 坐标轴标签(标题)
.chartXAxisLabel("Time", position: .bottom, alignment: .center)
.chartYAxisLabel("Revenue ($)", position: .leading, alignment: .center)
比例尺配置
.chartYScale(domain: 0...100) // 显式数值域
.chartYScale(domain: .automatic(includesZero: true)) // 包含零
.chartYScale(domain: 1...10000, type: .log) // 对数比例尺
.chartXScale(domain: ["Mon", "Tue", "Wed", "Thu"]) // 分类排序
前景样式与编码
BarMark(...).foregroundStyle(.blue) // 静态颜色
BarMark(...).foregroundStyle(by: .value("Category", item.category)) // 数据编码
AreaMark(...).foregroundStyle( // 渐变
.linearGradient(colors: [.blue, .cyan], startPoint: .bottom, endPoint: .top)
)
选择(iOS 17+)
@State private var selectedDate: Date?
@State private var selectedRange: ClosedRange<Date>?
@State private var selectedAngle: Double?
// 点选择
Chart(data) { item in
LineMark(x: .value("Date", item.date), y: .value("Value", item.value))
}
.chartXSelection(value: $selectedDate)
// 范围选择
.chartXSelection(range: $selectedRange)
// 角度选择绑定可绘制的角度值;从范围推导类别。
.chartAngleSelection(value: $selectedAngle)
可滚动图表(iOS 17+)
Chart(dailyData) { item in
BarMark(x: .value("Date", item.date, unit: .day), y: .value("Steps", item.steps))
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 7) // 7 天可见
.chartScrollPosition(initialX: latestDate)
.chartScrollTargetBehavior(
.valueAligned(matching: DateComponents(hour: 0), majorAlignment: .page)
)
标注
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
.annotation(position: .top, alignment: .center, spacing: 4) {
Text("\(item.sales, format: .number)").font(.caption2)
}
// 溢出处理
.annotation(
position: .top,
overflowResolution: .init(x: .fit(to: .chart), y: .padScale)
) { Text("Label") }
图例
.chartLegend(.hidden) // 隐藏
.chartLegend(position: .bottom, alignment: .center, spacing: 10) // 位置
.chartLegend(position: .bottom) { // 自定义
HStack {
ForEach(categories, id: \.self) { cat in
Label(cat, systemImage: "circle.fill").font(.caption)
}
}
}
向量化绘图(iOS 18+)
用于大数据集(1000+ 点)。接受整个集合或函数。
// 数据驱动
Chart {
BarPlot(sales, x: .value("Month", \.month), y: .value("Revenue", \.revenue))
.foregroundStyle(\.barColor)
}
// 函数绘图:y = f(x)
Chart {
LinePlot(x: "x", y: "y", domain: -5...5) { x in sin(x) }
}
// 参数方程:(x, y) = f(t)
Chart {
LinePlot(x: "x", y: "y", t: "t", domain: 0...(2 * .pi)) { t in
(x: cos(t), y: sin(t))
}
}
在简单值修饰符之前应用基于 KeyPath 的修饰符:
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.foregroundStyle(\.color) // 先 KeyPath
.opacity(0.8) // 后值修饰符
3D 图表(iOS 26+)
对空间数据或双变量曲面使用 Chart3D,不要将其作为普通 2D 分类或时间序列图表的装饰性替代品。Chart3D 接受 SurfacePlot 以及 PointMark、RuleMark 和 RectangleMark 的 3D 初始化器。
@State private var pose: Chart3DPose = .default
Chart3D {
SurfacePlot(x: "x", y: "y", z: "z") { x, z in
sin(2 * x) * cos(2 * z)
}
.foregroundStyle(.heightBased)
}
.chartXScale(domain: -2...2)
.chartYScale(domain: -1...1)
.chartZScale(domain: -2...2)
.chart3DPose($pose)
常见错误
1. 多折线图缺少 series 参数
// 错误——所有点连接成一条线
Chart {
ForEach(allCities) { item in
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
}
}
// 正确——每个城市一条独立的线
Chart {
ForEach(allCities) { item in
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
.foregroundStyle(by: .value("City", item.city))
}
}
2. SectorMark 切片过多
// 错误——20 个小切片难以阅读
Chart(twentyCategories, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
}
// 正确——分组为前 5 个 + "其他"
Chart(groupedData, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
.foregroundStyle(by: .value("Category", item.name))
}
3. 零基线重要时缺少比例尺域
// 错误——坐标轴从 ~95 开始;小变化看起来很大
Chart(data) {
LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
// 正确——显式域以诚实表示
Chart(data) {
LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
.chartYScale(domain: 0...100)
4. 静态 foregroundStyle 覆盖数据编码
// 错误——静态颜色覆盖了按值编码
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
.foregroundStyle(by: .value("Category", item.category))
.foregroundStyle(.blue)
// 正确——仅使用数据编码
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
.foregroundStyle(by: .value("Category", item.category))
5. 对 10,000+ 数据点使用单个标记
// 错误——创建 10,000 个标记视图;性能慢
Chart(largeDataset) { item in
PointMark(x: .value("X", item.x), y: .value("Y", item.y))
}
// 正确——向量化绘图(iOS 18+)
Chart {
PointPlot(largeDataset, x: .value("X", \.x), y: .value("Y", \.y))
}
6. 固定图表高度破坏动态类型
// 错误——大文本时裁剪坐标轴标签
Chart(data) { ... }
.frame(height: 200)
// 正确——自适应大小
Chart(data) { ... }
.frame(minHeight: 200, maxHeight: 400)
7. 向量化绘图上值修饰符在 KeyPath 修饰符之前
// 错误——编译错误
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.opacity(0.8)
.foregroundStyle(\.color)
// 正确——KeyPath 修饰符在前
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.foregroundStyle(\.color)
.opacity(0.8)
8. 缺少无障碍标签
// 错误——VoiceOver 用户得不到上下文
Chart(data) {
BarMark(x: .value("Month", $0.month), y: .value("Sales", $0.sales))
}
// 正确——为每个标记添加无障碍信息
Chart(data) { item in
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
.accessibilityLabel("\(item.month)")
.accessibilityValue("\(item.sales) units sold")
}
9. 将角度选择视为类别选择
chartAngleSelection(value:) 绑定选中的可绘制角度值。对于饼图和环形图,在比较类别标签之前,需要将该数值通过累积扇形范围进行映射。
审查清单
- [ ] 数据模型使用
Identifiable或图表使用id:键路径 - [ ] 标记类型与目标匹配(柱状图=比较,折线图=趋势,扇形=比例)
- [ ] 多系列折线使用
series:参数或.foregroundStyle(by:) - [ ] 坐标轴配置了适当的标签、刻度和网格线
- [ ] 当零基线重要时,显式设置比例尺域
- [ ] 饼图/环形图使用正值,5-7 个扇形,并分组为“其他”
- [ ] 选择绑定类型与坐标轴数据类型匹配(日期轴使用
Date?) - [ ] 饼图/环形图的角度选择将数值角度映射回类别
- [ ] 可滚动图表设置
.chartXVisibleDomain(length:)用于视口 - [ ] 数据集超过 1000 点时使用向量化绘图
- [ ] 向量化绘图上 KeyPath 修饰符在值修饰符之前
- [ ]
Chart3D仅用于真正的 3D 数据或曲面,并检查了 z 比例尺和姿态 - [ ] 为标记添加了无障碍标签以支持 VoiceOver
- [ ] 图表在动态类型和深色模式下测试
- [ ] 图例可见并定位,或有意隐藏
- [ ] 确保图表数据模型类型是 Sendable;在 @MainActor 上更新图表数据
参考资料
- 扩展模式:references/charts-patterns.md
- Apple 文档:Swift Charts
- Apple 文档:Creating a chart using Swift Charts
- Apple 文档:Swift Charts updates
- Apple 文档:Chart3D
- Apple 文档:SurfacePlot






