它是苹果在 2022年 WWDC 推出的一个框架 (iOS16.0+, macOS 13.0+ …),让我们可以用 SwiftUI 的语法来制作所有 Apple 平台都能使用的图表, 虽然我们可能由于版本支持情况暂时用不到, 不过了解一下也不错
使用它, 只需较少的代码就能制作有效且可定制的图表。这个框架提供了标记、刻度、坐标轴和图例等构建模块,所以我们可以将它们组合起来,开发各种数据驱动的图表。
目前我们在苹果的各种设备上系统自带 APP 的图表都是使用它制作的
比如股票,健康,天气 APP 中这些看起来差别很大的图表
乃至 Mac 上的硬盘容量,手机的电池使用情况, 到 Apple watch 上的运动,都用到了它
相信看到这里,大家应该对他的可定制化这一点有一些认识了
示例
BarMark
接下来让我们实际使用它做一做表格
假设我们现在要为一个公司小卖部做一个后台管理的统计 App, 它可以看每日的销量情况,可以看到具体的零食的销售趋势,单个用户的活跃程度,成本和销售价比例对比等等
那么先从先从简单的 Barmark(条形图) 开始吧
我们需要看到小卖部每天的销量的表格, 我们可以很简单就使用 BarMark 添加一个条形图来表示
// 创建图表
Chart(salesData.prefix(selectedRange), id: \\.id) { data in
// 创建条形图
BarMark(
x: .value("Date", data.date, unit: .day), // 日期
y: .value("Sales", data.sales) // 销售量
)
}
很简单的几行代码,就得到了上面的条形图
接下来为 X 轴添加日期,也很简单, 利用AxisMarks就好了,然后我们把格式设定为日
Chart(salesData.prefix(selectedRange), id: \\.id) { data in
BarMark(
x: .value("Date", data.date, unit: .day),
y: .value("Sales", data.sales)
)
}
chartXAxis {
// 设置X轴的格式
AxisMarks(values: .stride(by: .day)) { _ in
AxisValueLabel(format: .dateTime.weekday().day())
}
}
然后为了方便我们准确看到哪些天的销量比较高,我们打算添加一个过滤功能,让我们设定数量以上的 bar 颜色改成橙色
BarMark(
x: .value("Date", data.date, unit: .day),
y: .value("Sales", data.sales)
)
.foregroundStyle(data.sales > Int(threshold) ? aboveColor.gradient : belowColor.gradient) // 根据销售量设置颜色
然后添加一条辅助的线作为过滤的线, 在Chart 里添加一个 RuleMark 就好了
Chart(salesData.prefix(selectedRange), id: \\.id) { data in
BarMark(
x: .value("Date", data.date, unit: .day),
y: .value("Sales", data.sales)
)
// 创建阈值线
RuleMark(
y: .value("Threshold", threshold)
)
.lineStyle(StrokeStyle(lineWidth: 2))
.foregroundStyle(.red)
.annotation(position: .top, alignment: .leading) {
// 为阈值线添加标注(高度自定义)
Text("\\(threshold, specifier: "销量: %.0f 个")")
.font(.title2.bold())
.foregroundColor(.primary)
.background {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color(UIColor.systemBackground))
RoundedRectangle(cornerRadius: 8)
.fill(Color(UIColor.quaternarySystemFill).opacity(0.7))
}
.padding(.horizontal, -8)
.padding(.vertical, -4)
}
.padding(.bottom, 4)
}
}
最后我们的图表就完成了!
更多样式
我们通过一些基本的组件成功组合成了想要的表格
由于 Swift Charts 这个声明式的框架理念是不用预先设置好的表格样式,而是给你基本的组件组合,所以我们想要更改表格的表现形式也很容易
比如各种不同的 Mark 形式,和一些附加的表格信息添加

比如我们只需要简单的把 BarMark 替换为AreaMark
LineMark
PointMark
等等
BarMark(
x: .value("Date", data.date, unit: .day),
y: .value("Sales", data.sales)
)
就能让刚刚的表格看起来非常不同

我们还能为表格添加更多样式,比如为了方便用户,我添加一根划分每周的虚线
或者调转 X 和 Y 轴让表格横向显示

高度可定制
总之 Swift Charts 不是一个个预置样式的图表组件让你填充数据, 而是提供一些基本构件让我们可以自由组合出不同样式
并且支持 Dark Mode / Device Screen Sizes / Dynamic Type / Voice Over / Audio Graphs / High-Contrast / Localization / Multi-Platform 等等系统功能
所以接下来让我们尝试组合一下各种 mark,来表示不同零食的销量

比如我们用 line + symbol
Chart {
// 每种零食
ForEach(selectedSnacks, id: \\.self) { snack in
// 每个零食的数据
ForEach(salesData.filter { $0.snack == snack }, id: \\.id) { data in
LineMark(
x: .value("Date", data.date, unit: .day),
y: .value("Sales", data.sales)
)
.interpolationMethod(.cardinal) // 连线的方式
.foregroundStyle(by: .value("Snack", data.snack))
**.symbol(by: .value("Snack", data.snack))**
}
}
}
结合了曲线和图标来让表格更适合查看不同零食之间的销售趋势对比

甚至更加定制化一点,我们可以用它来制作一个类似 Github 的热力图那样的表格来代表用户每天购买的零食数量 只需要使用 RectangleMark 就好了
Chart(heatmapData) { data in
**RectangleMark**(
xStart: .value("xStart", relativeWeek(date: data.date)),
xEnd: .value("xEnd", relativeWeek(date: data.date) + 1),
yStart: .value("yStart", dayOfTheWeek(date: data.date)),
yEnd: .value("yEnd", dayOfTheWeek(date: data.date) + 1)
)
.foregroundStyle(by: .value("Sales", data.sales))
}
.chartForegroundStyleScale(range: Gradient(colors: [.white, .green]))
.chartLegend(.hidden)
.chartYAxis {
AxisMarks(values: .automatic(desiredCount: 7, roundLowerBound: false, roundUpperBound: false)) { _ in
AxisGridLine(stroke: .init(lineWidth: 1))
}
}
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 20, roundLowerBound: false, roundUpperBound: false)) { _ in
AxisGridLine(stroke: .init(lineWidth: 1))
}
}
.chartYScale(domain: .automatic(reversed: true))
.aspectRatio(20.0/7.0, contentMode: .fit)
.padding()
或者像Apple watch 中每天的圆环那样,制作同心圆来对比零食的成本和售价的比例

ZStack {
// 外圈
Chart(data) { snack in
SectorMark(
angle: .value("Sales", snack.sales),
innerRadius: .ratio(0.65),
outerRadius: .ratio(1.0)
)
.foregroundStyle(by: .value("Snack", snack.name))
}
// 内圈
Chart(data) { snack in
SectorMark(
angle: .value("Cost", snack.cost),
innerRadius: .ratio(0.3),
outerRadius: .ratio(0.55)
)
.foregroundStyle(by: .value("Snack", snack.name))
}
}
或者像这样可以互动选择图表上的范围并在下面展示数据

或者多平台的支持
注意看右边还有向散点图结合美国地图轮廓这样自定义形状展示设施情况的图表
LinePlot(
contiguousUSABorderCoordinates,
x: .value("Longitude", \\.mapProjection.x),
y: .value("Latitude", \\.mapProjection.y)
)
.interpolationMethod(.catmullRom)
.lineStyle(.init(lineWidth: 1, lineCap: .round, lineJoin: .round))
.foregroundStyle(.gray)
PointPlot(
model.filteredData,
x: .value("Longitude", \\.mapProjection.x),
y: .value("Latitude", \\.mapProjection.y)
)
.symbolSize(by: .value("Capacity", \\.capacityDC))
.foregroundStyle(by: .value("Breakdown", model.breakdownField.keyPath))
它还有更多的功能我们就不深入了, 比如它还支持动画, 无障碍, 和图表互动等等
今年的更新
刚刚过去的 WWDC 上,苹果也为他带来了进一步的更新,让他可以将数据以外的内容可视化
比如各种数据函数的绘制, 在数据分析领域很有用
甚至今年 iPad 上终于推出的计算器 App 中手写函数绘制图像的功能也是由它实现的
总之这是一个非常灵活和支持大量系统特性的表格框架, 以后有机会不妨尝试一下吧