关于 swift-charts
面向 Swift 开发者的 Swift Charts 可视化技能,介绍二维与三维图表、标记、坐标轴、选择和滚动,并包含大数据集及无障碍检查建议。
主要功能
- 01组合多种图表标记
- 02配置坐标轴、比例尺和图例
- 03添加选择、滚动与注释
- 04使用向量化绘图处理大数据集
- 05构建空间数据三维图表
- 06检查无障碍与图表状态
适用场景
- 01展示分类比较和时间趋势
- 02为图表添加交互式数据选择
- 03绘制超过 1000 个数据点
- 04呈现空间数据或双变量曲面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swift-charts安装后在 Claude Code 中输入/swift-charts确认已加载。
附带文件evals/evals.jsonreferences/charts-patterns.md
详细介绍
Swift Charts
使用 Swift Charts 构建数据可视化,目标平台为 iOS 26+。在 Chart 或 Chart3D 中组合标记,使用视图修饰符配置坐标轴和比例尺,并在数据适用时使用向量化绘图或 3D 绘图。
参见 references/charts-patterns.md,了解更多模式、3D 图表、无障碍功能和主题配置指南。
目录
- 工作流
- 图表容器
- 标记类型
- 坐标轴自定义
- 比例尺配置
- 前景样式和编码
- 选择(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+)
// Vertical bar
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
// Stacked by category (automatic when same x maps to multiple bars)
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
.foregroundStyle(by: .value("Product", item.product))
// Horizontal bar
BarMark(x: .value("Sales", item.sales), y: .value("Month", item.month))
// Interval bar (Gantt chart)
BarMark(
xStart: .value("Start", item.start),
xEnd: .value("End", item.end),
y: .value("Task", item.task)
)
LineMark(iOS 16+)
// Single line
LineMark(x: .value("Date", item.date), y: .value("Price", item.price))
// Multi-series via foregroundStyle encoding
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
.foregroundStyle(by: .value("City", item.city))
.interpolationMethod(.catmullRom)
// Multi-series with explicit series parameter
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+)
// Stacked area
AreaMark(x: .value("Date", item.date), y: .value("Sales", item.sales))
.foregroundStyle(by: .value("Category", item.category))
// Range band
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 仅适用于严格大于零的值;对于饼图或圆环图之外的零值和负值,请进行筛选、聚合或说明。
// Pie chart
Chart(data, id: \.name) { item in
SectorMark(angle: .value("Sales", item.sales))
.foregroundStyle(by: .value("Category", item.name))
}
// Donut chart
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))
}
坐标轴自定义
// Hide axes
.chartXAxis(.hidden)
.chartYAxis(.hidden)
// Custom axis content
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
}
// Multiple AxisMarks compositions (different intervals for grid vs. labels)
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { _ in AxisGridLine() }
AxisMarks(values: .stride(by: .week)) { _ in
AxisTick()
AxisValueLabel(format: .dateTime.week())
}
}
// Axis labels (titles)
.chartXAxisLabel("Time", position: .bottom, alignment: .center)
.chartYAxisLabel("Revenue ($)", position: .leading, alignment: .center)
比例尺配置
.chartYScale(domain: 0...100) // Explicit numeric domain
.chartYScale(domain: .automatic(includesZero: true)) // Include zero
.chartYScale(domain: 1...10000, type: .log) // Logarithmic scale
.chartXScale(domain: ["Mon", "Tue", "Wed", "Thu"]) // Categorical ordering
前景样式和编码
BarMark(...).foregroundStyle(.blue) // Static color
BarMark(...).foregroundStyle(by: .value("Category", item.category)) // Data encoding
AreaMark(...).foregroundStyle( // Gradient
.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?
// Point selection
Chart(data) { item in
LineMark(x: .value("Date", item.date), y: .value("Value", item.value))
}
.chartXSelection(value: $selectedDate)
// Range selection
.chartXSelection(range: $selectedRange)
// Angular selection binds the plottable angle value; derive the category from ranges.
.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 days visible
.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)
}
// Overflow resolution
.annotation(
position: .top,
overflowResolution: .init(x: .fit(to: .chart), y: .padScale)
) { Text("Label") }
图例
.chartLegend(.hidden) // Hide
.chartLegend(position: .bottom, alignment: .center, spacing: 10) // Position
.chartLegend(position: .bottom) { // Custom
HStack {
ForEach(categories, id: \.self) { cat in
Label(cat, systemImage: "circle.fill").font(.caption)
}
}
}
向量化绘图(iOS 18+)
适用于大型数据集(1000 个以上的数据点)。可以接受整个集合或函数。
// Data-driven
Chart {
BarPlot(sales, x: .value("Month", \.month), y: .value("Revenue", \.revenue))
.foregroundStyle(\.barColor)
}
// Function plotting: y = f(x)
Chart {
LinePlot(x: "x", y: "y", domain: -5...5) { x in sin(x) }
}
// Parametric: (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 first
.opacity(0.8) // Value modifier second
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. 多折线图缺少系列参数
// WRONG -- all points connect into one line
Chart {
ForEach(allCities) { item in
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
}
}
// CORRECT -- separate lines per city
Chart {
ForEach(allCities) { item in
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
.foregroundStyle(by: .value("City", item.city))
}
}
2. SectorMark 切片过多
// WRONG -- 20 tiny sectors are unreadable
Chart(twentyCategories, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
}
// CORRECT -- group into top 5 + "Other"
Chart(groupedData, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
.foregroundStyle(by: .value("Category", item.name))
}
3. 需要零基线时缺少比例尺取值范围
// WRONG -- axis starts at ~95; small changes look dramatic
Chart(data) {
LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
// CORRECT -- explicit domain for honest representation
Chart(data) {
LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
.chartYScale(domain: 0...100)
4. 静态 foregroundStyle 覆盖数据编码
// WRONG -- static color overrides by-value encoding
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
.foregroundStyle(by: .value("Category", item.category))
.foregroundStyle(.blue)
// CORRECT -- use only the data encoding
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
.foregroundStyle(by: .value("Category", item.category))
5. 为 10,000 个以上的数据点分别创建标记
// WRONG -- creates 10,000 mark views; slow
Chart(largeDataset) { item in
PointMark(x: .value("X", item.x), y: .value("Y", item.y))
}
// CORRECT -- vectorized plot (iOS 18+)
Chart {
PointPlot(largeDataset, x: .value("X", \.x), y: .value("Y", \.y))
}
6. 固定图表高度导致动态字体无法正常工作
// WRONG -- clips axis labels at large text sizes
Chart(data) { ... }
.frame(height: 200)
// CORRECT -- adaptive sizing
Chart(data) { ... }
.frame(minHeight: 200, maxHeight: 400)
7. 向量化绘图中的值修饰符之后使用 KeyPath 修饰符
// WRONG -- compiler error
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.opacity(0.8)
.foregroundStyle(\.color)
// CORRECT -- KeyPath modifiers first
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.foregroundStyle(\.color)
.opacity(0.8)
8. 缺少辅助功能标签
// WRONG -- VoiceOver users get no context
Chart(data) {
BarMark(x: .value("Month", $0.month), y: .value("Sales", $0.sales))
}
// CORRECT -- add per-mark accessibility
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 修饰符,再应用值修饰符
- 仅在处理真实 3D 数据或曲面时使用
Chart3D,并检查 z 轴比例尺和视角 - 为标记添加辅助功能标签,以支持 VoiceOver
- 使用动态字体和深色模式测试图表
- 显示并定位图例,或有意将其隐藏
- 确保图表数据模型类型符合 Sendable;在 @MainActor 上更新图表数据
参考资料
- 扩展模式:references/charts-patterns.md
- Apple 文档:Swift Charts
- Apple 文档:使用 Swift Charts 创建图表
- Apple 文档:Swift Charts 更新
- Apple 文档:Chart3D
- Apple 文档:SurfacePlot