在 iOS 16 的新版 SwiftUI 中,我最喜欢的其中一个功能就是 Charts 框架。在 iOS 16 之前,我们需要构建自己的图表、或是依靠第三方程序库来建立图表。Apple 推出了这个新框架,开发者就可以更轻松地创建动画化和互动的图表。
简单来说,我们只需要定义 Mark,就可以构建出 SwiftUI 图表。让我们看看这个简单的例子:
import SwiftUI
import Charts
struct ContentView: View {
var body: some View {
Chart {
BarMark(
x: .value("Day", "Monday"),
y: .value("Steps", 6019)
)
BarMark(
x: .value("Day", "Tuesday"),
y: .value("Steps", 7200)
)
}
}
}
无论我们想要构建长条图还是折线图,我们都会从 Chart
视图开始。在图表里面,我们可以定义 bar mark,来提供图表数据。BarMark
视图是用来构建长条图的,每一个 BarMark
视图都会有 x
和 y
值,x
值就是代表 x 轴的图表数据,如此类推。在以上的代码中,我把 x
轴的标签设置为 Day,而 y
轴就是总步数。
让我们在 Xcode 14 输入以上代码,预览就会自动显示有两个垂直长方体的长条图。
以上就是创建长条图最简单的方法。不过,我们通常都不会对图表数据进行硬编码 (hardcode),而是在 Charts API 编写一组数据。让我们看看以下例子:
struct ContentView: View {
let weekdays = Calendar.current.shortWeekdaySymbols
let steps = [ 10531, 6019, 7200, 8311, 7403, 6503, 9230 ]
var body: some View {
Chart {
ForEach(weekdays.indices, id: \.self) { index in
BarMark(x: .value("Day", weekdays[index]), y: .value("Steps", steps[index]))
}
}
}
}
我们为图表数据创建了两个数组(weekdays
和steps
)。 在 Chart
视图中,我们读取 weekdays
数组并显示图表数据。 如果你已在 Xcode 项目中输入代码,预览部分应该呈现如图 38.2 所示的条形图。
在默认情况下,Charts API 会以相同颜色呈现所有长方体。如果我们想把每个长方体设置为不同的颜色,可以将 foregroundStyle
修饰符附加到 BarMark
视图:
.foregroundStyle(by: .value("Day", weekdays[index]))
如果我们想为所有长方体添加注释,可以使用 annotation
修饰器:
.annotation {
Text("\(steps[index])")
}
作出这些改动后,长条图就更加漂亮了。
如果想要建立横向的长条图,我们只需要把 BarMark
视图内的 x
和 y
参数 (parameter) 交换就可以了。
你已学会如何建立长条图,现在我们会示范使用 SwiftUI Charts API,来构建一个折线图,显示 2021 年 7 月至 2022 年 6 月香港、台北和伦敦的平均气温。
让我们先建立一个 WeatherData
结构,来储存天气数据。在你的 Xcode 项目中,使用 Swift File 模板创建一个名为 WeatherData
的新文件。 在文件中加入以下代码:
struct WeatherData: Identifiable {
let id = UUID()
let date: Date
let temperature: Double
init(year: Int, month: Int, day: Int, temperature: Double) {
self.date = Calendar.current.date(from: .init(year: year, month: month, day: day)) ?? Date()
self.temperature = temperature
}
}
let hkWeatherData = [
WeatherData(year: 2021, month: 7, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 8, day: 1, temperature: 29.0),
WeatherData(year: 2021, month: 9, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 10, day: 1, temperature: 26.0),
WeatherData(year: 2021, month: 11, day: 1, temperature: 23.0),
WeatherData(year: 2021, month: 12, day: 1, temperature: 19.0),
WeatherData(year: 2022, month: 1, day: 1, temperature: 18.0),
WeatherData(year: 2022, month: 2, day: 1, temperature: 15.0),
WeatherData(year: 2022, month: 3, day: 1, temperature: 22.0),
WeatherData(year: 2022, month: 4, day: 1, temperature: 24.0),
WeatherData(year: 2022, month: 5, day: 1, temperature: 26.0),
WeatherData(year: 2022, month: 6, day: 1, temperature: 29.0)
]
let londonWeatherData = [
WeatherData(year: 2021, month: 7, day: 1, temperature: 19.0),
WeatherData(year: 2021, month: 8, day: 1, temperature: 17.0),
WeatherData(year: 2021, month: 9, day: 1, temperature: 17.0),
WeatherData(year: 2021, month: 10, day: 1, temperature: 13.0),
WeatherData(year: 2021, month: 11, day: 1, temperature: 8.0),
WeatherData(year: 2021, month: 12, day: 1, temperature: 8.0),
WeatherData(year: 2022, month: 1, day: 1, temperature: 5.0),
WeatherData(year: 2022, month: 2, day: 1, temperature: 8.0),
WeatherData(year: 2022, month: 3, day: 1, temperature: 9.0),
WeatherData(year: 2022, month: 4, day: 1, temperature: 11.0),
WeatherData(year: 2022, month: 5, day: 1, temperature: 15.0),
WeatherData(year: 2022, month: 6, day: 1, temperature: 18.0)
]
let taipeiWeatherData = [
WeatherData(year: 2021, month: 7, day: 1, temperature: 31.0),
WeatherData(year: 2021, month: 8, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 9, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 10, day: 1, temperature: 26.0),
WeatherData(year: 2021, month: 11, day: 1, temperature: 22.0),
WeatherData(year: 2021, month: 12, day: 1, temperature: 19.0),
WeatherData(year: 2022, month: 1, day: 1, temperature: 17.0),
WeatherData(year: 2022, month: 2, day: 1, temperature: 17.0),
WeatherData(year: 2022, month: 3, day: 1, temperature: 21.0),
WeatherData(year: 2022, month: 4, day: 1, temperature: 23.0),
WeatherData(year: 2022, month: 5, day: 1, temperature: 24.0),
WeatherData(year: 2022, month: 6, day: 1, temperature: 27.0)
]
因为 Chart initializer 接受 Identifiable
物件的列表,所以我们要让 WeatherData
遵守 Identifiable
协议。我们要为每个城市创建一个数组 (array),来储存天气数据。
在项目导航器中,使用 SwiftUI View 模板创建一个名为 SimpleLineChartView
的新文件。不论我们要利用 Charts 框架建立什么图表,都需要先汇入 Charts
框架:
import Charts
然后,我们声明一个数组去储存三个城市的天气数据:
let chartData = [ (city: "Hong Kong", data: hkWeatherData),
(city: "London", data: londonWeatherData),
(city: "Taipei", data: taipeiWeatherData) ]
在body
变量中,像这样修改代码以创建折线图:
VStack {
Chart {
ForEach(hkWeatherData) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
}
.frame(height: 300)
}
以上的代码绘制了一个折线图,来显示香港的平均气温。ForEach
语句 loop through 储存在 hkWeatherData
中的所有项目。我们会为每个项目创建一个 LineMark
物件,当中 x
轴设置为日期,而 y
轴则设置为平均气温。
我们也可以选择使用 frame
修饰符,来调整图表的大小。如果我们在 Xcode 预览中预览代码,应该会看到如图 38.5 的折线图:
我们可以利用 chartXAxis
和 chartYAxis
修饰符,来客制化 x 和 y 轴。比如说,如果我们想以数字格式显示月份,我们可以将 chartXAxis
修饰符附加到 Chart
视图:
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.defaultDigits))
}
}
在 chartXAxis
中,我们为月份的数值创建了一个 AxisMarks
的视觉标记 (visual mark)。针对每个数值,我们可以使用特定格式显示一个 ValueLabel。以下这行代码就告诉了 SwiftUI 图表,我们想要使用数字格式显示月份:
.dateTime.month(.defaultDigits)
另外,我们也使用了 AxisGridLine
来添加一些 grid line。
至于 y 轴,我们之前是在后面(右侧)显示 y 轴的,我们想改为在前面(左侧)显示。让我们如此附加 chartYAxis
修饰符:
.chartYAxis {
AxisMarks(position: .leading)
}
做好改动之后,Xcode 预览应该会把图表修改如下。y 轴会在左侧显示,而月份的格式也会变成以数字格式显示。另外,你也应该会看到 grid line。
我们可以利用 chartPlotStyle
修饰器,来修改绘图区域的背景颜色。让我们将修饰符附加到 Chart
视图:
.chartPlotStyle { plotArea in
plotArea
.background(.blue.opacity(0.1))
}
然后,我们就可以使用 background
修饰符修改绘图区域的颜色。在上面的例子中,我们把绘图区域修改成浅蓝色。
现在,图表只显示单一数据(香港的天气数据),那我们如何把伦敦和台北的天气数据显示在同一个折线图中呢?
我们可以这样重写 Chart
视图的代码:
Chart {
ForEach(chartData, id: \.city) { series in
ForEach(series.data) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
.foregroundStyle(by: .value("City", series.city))
}
}
我们有另一个 ForEach
来读取三个城市的数据。我们在这里使用了 foregroundStyle
修饰符,为每条线应用不同的颜色。我们不需要指定颜色,SwiftUI 会自动选择颜色。
现在,三个城市的符号都相同。如果要使用不同的符号,让我们在 foregroundStyle
之后添加这行代码:
.symbol(by: .value("City", series.city))
如此一来,不同城市就会有不同的符号了。
我们可以把 interpolationMethod
修饰器附加到 LineMark
,来修改折线图的内插方法。
.interpolationMethod(.stepStart)
如果我们把内插方法设置为 .stepStart
,折线图就会变成这样:
除了 .stepStart
之外,我们还可以使用以下设定:
Charts 框架是 SwiftUI 一个很好的新功能,即使是 SwiftUI 的初学者,用几行代码,就可以构建出漂亮的图表。虽然这篇教学文章以折线图为例子,但其实我们可以利用 Charts API 轻松地将折线图转换为其他图表,例如长条图。你可以参阅 Swift Charts 文档深入了解这个 API。
在本章所准备的示例档中,有最后完整的 Xcode 项目,可供你下载参考: