SwiftUI 的 Charts 是一个非常强大且直观的框架,专门用于创建数据可视化图表。它引入了简洁的 API 来轻松创建各种类型的图表,比如柱状图、折线图、饼图等。以下是一些关于 SwiftUI Charts 的关键概念和使用方式。
基本结构
在 SwiftUI 中创建图表,需要导入 Charts 模块
import Charts
使用 Chart 视图:
import SwiftUI
import Charts
struct ContentView: View {
var data: [SalesData] = [
SalesData(month: "Jan", sales: 200),
SalesData(month: "Feb", sales: 150),
SalesData(month: "Mar", sales: 300)
]
var body: some View {
Chart(data) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Sales", item.sales)
)
}
.frame(height: 300)
.padding()
}
}
struct SalesData: Identifiable {
let id = UUID()
let month: String
let sales: Int
}

支持的图表类型
SwiftUI Charts 支持多种类型的图表,每种类型都有其独特的 Mark,用来绘制对应的图表形态。以下是 SwiftUI Charts 目前支持的主要图表类型:
1、柱状图 (Bar Chart)
标记类型: BarMark
用于展示分类数据之间的比较。
既支持垂直柱状图,也支持水平柱状图。
Chart(data) {
BarMark(
x: .value("Category", $0.category),
y: .value("Value", $0.value)
)
}

2、折线图 (Line Chart)
标记类型: LineMark
用于表示连续数据的趋势。
通常用在时间序列数据的可视化中。
Chart(data) {
LineMark(
x: .value("Time", $0.time),
y: .value("Value", $0.value)
)
}

3、面积图 (Area Chart)
标记类型: AreaMark
用于展示数据随时间的变化,并填充曲线下方的区域。
可以叠加多个 AreaMark 创建堆叠图。
Chart(data) {
AreaMark(
x: .value("Date", $0.date),
y: .value("Count", $0.count)
)
}

4、散点图 (Scatter Plot)
标记类型: PointMark
用于表示两个变量之间的关系。
可以通过形状、大小或颜色来区分类别。
Chart(data) {
PointMark(
x: .value("X Value", $0.x),
y: .value("Y Value", $0.y)
)
}

5、饼图 / 环形图 (Pie/Donut Chart)
标记类型: 使用 SectorMark
适用于展示分类数据的占比。
通过调整起点和终点角度绘制。
Chart(data) { item in
SectorMark(
angle: .value("Sales", item.sales)
)
.foregroundStyle(by: .value("Month", item.month))
}

SectorMark默认情况下,它会将所有数据的数值加总,并根据每个数据占总数的比例生成一个完整的圆。数据被映射成扇形角度,但它们没有视觉上的区分,导致它们叠加在一起,看起来像一个单一的圆。
因此,希望饼图能按某个参数分区并以不同颜色显示,可以通过设置 foregroundStyle 来实现。
6、组合图表 (Combination Charts)
组合标记: 使用多种 Mark 叠加。
例如,将柱状图和折线图组合在一起:
Chart(data) {
BarMark(
x: .value("Category", $0.month),
y: .value("Value", $0.sales)
)
LineMark(
x: .value("Category", $0.month),
y: .value("Value", $0.sales)
)
}

7、雷达图 (Radar Chart)
目前 Radar Charts 并未原生支持,需要通过自定义绘制来实现。
8、条形图 (Horizontal Bar Chart)
标记类型: 水平的 BarMark,将 x 和 y 值互换。
Chart(data) {
BarMark(
x: .value("Value", $0.sales),
y: .value("Category", $0.month)
)
}

自定义图表样式
颜色:可以自定义每个 Mark 的颜色。
图例:通过 .chartLegend() 添加图例。
轴样式:自定义轴线、刻度和标签。
BarMark(
x: .value("Category", "Electronics"),
y: .value("Revenue", 1000)
)
.foregroundStyle(Color.blue)
数据动态更新
Charts 的数据源可以绑定到动态数据模型,从而支持实时更新。
@State private var salesData = [
SalesData(month: "Jan", sales: 200),
SalesData(month: "Feb", sales: 150)
]
Button("Add Data") {
salesData.append(SalesData(month: "Mar", sales: 300))
}
Mark参数
在 SwiftUI 的 Charts 中,每个 Mark 都有多个参数,因为它们提供了高度的灵活性和可定制性。这些参数控制图表的外观和行为,使其适配不同的数据结构和可视化需求。
参考示例
struct SampleRating {
let place: String
let rating: Int
static let ratings: [SampleRating] = [
SampleRating(place: "Bellagio", rating: 88),
SampleRating(place: "Paris", rating: 75),
SampleRating(place: "Treasure Island", rating: 33),
SampleRating(place: "Excalibur", rating: 88)
]
}
struct VegasChart: View {
var body: some View {
Chart(SampleRating.ratings, id: \.place) { rating in
SectorMark(angle: .value("Ratings", rating.rating), innerRadius: .ratio(0.5), angularInset: 1)
.cornerRadius(7)
.foregroundStyle(by: .value("Place", rating.rating))
}
.padding()
.frame(height: 500)
}
}

在这段SectorMark的代码中:
1)angle控制扇形的角度大小:.value(“Ratings”, rating.rating) 表示从数据中取 rating.rating 作为角度值,并将该数据绑定到图表的 “Ratings” 维度。
2)innerRadius设置扇形的内半径:.ratio(0.5) 表示内半径是图表外半径的 50%。
3)angularInset设置扇形的间隔角度:angularInset: 1 表示在每个扇形之间留出 1° 的间隙, 提高了可视化的清晰度,特别是在扇形数据较多时。
4)cornerRadius设置扇形的边缘圆角:.cornerRadius(7) 会使扇形的外边缘变得平滑。
5)foregroundStyle设置扇形的不同颜色:.value(“Place”, rating.rating) 将数据绑定到 “Place” 维度, Charts 会自动为不同的 “Place” 分配不同的颜色。通过颜色区分数据类别,提高数据的可读性。
高级功能
组合图表:在一个图表中叠加不同类型的 Mark。
动画效果:通过 withAnimation 添加动态效果。
筛选数据:使用 SwiftUI 的 Filter。
隐藏图例:
在饼图中通常会自动生成图例和注释,如果图例影响到布局,可以通过chartLegend(.hidden)进行隐藏:
Chart(data, id: \.PiggyBank) { item in
SectorMark(
...
)
}
.chartLegend(.hidden) // 隐藏默认的图例

实用场景
数据可视化:展示统计数据(如财务报表、用户增长趋势)。
健康和活动:显示步数、心率等。
科学实验:展示实验结果图表。
实际应用

例如,我想要在存钱罐的应用中,将现有的目标金额和当前金额两个字段,转换为饼图,以便更清晰的了解到存钱的进度。
这个DetailView就是显示的视图,有两个金额参数:
struct DetailView: View {
var CurrentAmount: Double // 当前金额
var TargetAmount: Double // 目标金额
var body: some View {
// 可视化图表
}
}
1、数据结构
首先,我需要创建一个数组结构,作为Chart组件的数据源:
var data: [(label: String, value: Double, color: Color)] {
return [
(String(localized:"Deposit"), CurrentAmount, .blue), // 已存入金额
(String(localized:"Remaining"), max(TargetAmount - CurrentAmount,0), .gray)
]
}
data 是一个数组,包含 (label, value, color) 三个属性:
label:存钱罐的分类(“Deposit” 和 “Remaining”),用于本地化显示。
value:代表金额(当前存入金额、剩余金额)。
color:指定该类别的颜色(蓝色表示已存入,灰色表示剩余部分)。
2、创建Chart组件
Chart(data, id: \.label) { item in ...}
Chart 组件使用 data 数组作为数据源,并指定 label 作为唯一标识。
id: \.label 确保每个扇形区域在数据变化时正确更新。
3、SectorMark(扇形图)
SectorMark(
angle: .value("Amount", item.value),
innerRadius: .ratio(0.5),
outerRadius: .ratio(1),
angularInset: 1
)
SectorMark 是 Swift Charts 用于绘制 饼图(Pie Chart)的 Mark 类型。
1)angle: .value(“Amount”, item.value)
以 item.value 作为角度数据,即已存入金额和剩余金额的数值决定了扇形的大小。
2)innerRadius: .ratio(0.5)
内半径设定为 50%(0.5),意味着这是一个 环形图(Donut Chart)。
3)outerRadius: .ratio(1)
外半径为 100%(1),确保图形铺满指定的 Chart 区域。
4)angularInset: 1
使扇形之间有 1 度 的间隙,增强视觉分离感。

需要注意的是,angle: .value(“Amount”, item.value),这里的”Amount” 是数据键(Data Key),代表该数据点的角度来源。
item.value 决定了扇形的大小(例如存入金额 vs 剩余金额)。
但 “Amount” 这个标签不会自动显示在图表上,它只是一个标识符,供 Swift Charts 内部使用。
4、foregroundStyle 设定颜色
SectorMark设置foregroundStyle设置前景色:
.foregroundStyle(item.color)

如果想要使用Charts默认颜色,可以使用foregroundStyle(by:):
.foregroundStyle(by: .value("Amount", item.label))

foregroundStyle:根据 item.label 赋予不同的颜色。
by: .value(“Amount”, item.label) 让 Swift Charts 自动匹配相同 label 的数据点。
5、完整代码
struct DetailView: View {
var CurrentAmount: Double
var TargetAmount: Double
var data:[(label: String, value: Double, color: Color)] {
return [
(String(localized:"Deposit"), CurrentAmount, .blue), // 已存入金额
(String(localized:"Remaining"), max(TargetAmount - CurrentAmount,0),.gray)
]
}
var body: some View {
Chart(data, id: \.label) { item in
SectorMark(
angle: .value("Amount", item.value),
innerRadius: .ratio(0.5),
outerRadius: .ratio(1),
angularInset: 1
)
.cornerRadius(7)
.foregroundStyle(by: .value("Amount", item.label))
}
.frame(width: 120, height: 170)
.padding(20)
}
}

总结
SwiftUI Charts 是高度灵活的工具,可以通过以上类型以及自定义样式满足大多数数据可视化需求。
参考文章
iOS 18, SwiftUI 6, & Swift 6: 从零开始构建iOS应用程序, 涵盖visionOS, macOS, watchOS:https://www.bilibili.com/video/BV1b6421f7Px?spm_id_from=333.788.videopod.episodes&vd_source=f21219cb93118beac6a36b0ef961df6a&p=11
扩展知识
Argument ‘x’ must precede argument ‘y’报错
如果遇到以下报错:
Argument 'x' must precede argument 'y'
Replace 'y: .value("Category", $0.month),
x: .value("Value", $0.sales)' with 'x: .value("Value", $0.sales), y: .value("Category", $0.month)'
问题原因为:在 SwiftUI 的 Charts 框架中,参数的顺序有特定要求。BarMark 的参数顺序规定 x 必须在 y 之前。提示 Argument ‘x’ must precede argument ‘y’ 的原因正是因为代码中将 y 参数写在了 x 参数之前,违反了 API 的定义。
解决方案:根据提示修改参数的顺序即可。
BarMark(
x: .value("Value", $0.sales), // x 参数在前
y: .value("Category", $0.month) // y 参数在后
)
设置X轴和Y轴的范围
如果想要调整图表的X轴和Y轴范围,可以通过chartXScale和chartYScale进行调整:
Chart(SampleRating.ratings, id: \.trip) { rating in
PointMark(x: .value("Year", rating.trip), y: .value("Ratings", rating.trip - 2020 + 88))
LineMark(x: .value("Year", rating.trip), y: .value("Ratings", rating.rating))
}
.chartXScale(domain: 2020...2024)
.chartYScale(domain: 1...100)

chartXScale 和 chartYScale 是用于设置图表的 X 轴 和 Y 轴范围的修饰符。它们控制图表显示数据的范围,并帮助调整坐标轴的显示。
chartXScale(domain:):用于设置 X 轴的显示范围。可以定义一个 domain,例如 2020…2025,表示 X 轴的范围从 2020 到 2025。
chartYScale(domain:):用于设置 Y 轴的显示范围。可以定义一个 domain,例如 1…100,表示 Y 轴的范围从 1 到 100。