1、前言
Swift提供了Charts框架,使得我們可以通過簡(jiǎn)單的代碼即可在iOS、iPad、Mac和Apple watch等設(shè)備中顯示圖表,并且支持自定義,輕易實(shí)現(xiàn)各種風(fēng)格的圖表。這一篇我們繼續(xù)關(guān)注Apple watch,將數(shù)據(jù)以圖表的形式顯示在Apple Watch中。例如這是我最近做的項(xiàng)目截圖,以折線圖顯示用戶當(dāng)天Hrv數(shù)據(jù)的變化:
這是蘋果官方App中使用Charts的示例圖:

2、創(chuàng)建項(xiàng)目
同樣的這系列文章主要介紹Apple watch開發(fā),所以本章示例也只創(chuàng)建watch app:

項(xiàng)目名為ChartsDemo,勾選Watch-only App:

3、顯示柱狀圖
每一個(gè)圖表,為一個(gè)Chart組件,如果是柱狀圖,每一柱形為一個(gè)BarMark組件,例如某奶茶店有廣州和深圳兩家店鋪,要用組狀圖顯示某天的銷售量,完整示例代碼如下:
import SwiftUI
import Charts
struct ContentView: View {
var body: some View {
Chart() {
BarMark(
x: .value("銷量", 916),
y: .value("地區(qū)", "廣州")
)
BarMark(
x: .value("銷量", 1190),
y: .value("地區(qū)", "深圳")
)
}
}
}
#Preview {
ContentView()
}

框架會(huì)默認(rèn)會(huì)根據(jù)數(shù)據(jù),給我們?cè)O(shè)置xy軸的范圍。
在日常開發(fā)中,一般都是數(shù)據(jù)驅(qū)動(dòng)視圖,所以示例中的核心代碼應(yīng)該為:
let data = [
(city:"廣州",sales:916),
(city:"深圳",sales:1190),
(city:"北京",sales:1890),
(city:"上海",sales:1497)
]
var body: some View {
Chart() {
ForEach(data,id: \.city) {
BarMark(
x: .value("銷量", $0.sales),
y: .value("城市", $0.city)
)
}
}
}

我們可以進(jìn)一步簡(jiǎn)化上面的代碼:
var body: some View {
Chart(data,id: \.city) {
BarMark(
x: .value("銷量", $0.sales),
y: .value("城市", $0.city)
)
}
}
注意:如果有兩條city名稱一樣的數(shù)據(jù),框架會(huì)自動(dòng)合并成一條數(shù)據(jù)
3、顯示折線圖
先上代碼和運(yùn)行效果:
import SwiftUI
import Charts
struct ContentView: View {
let data = [
(day:Util.getDate(offset:1),sales:1666),
(day:Util.getDate(offset:2),sales:1899),
(day:Util.getDate(offset:3),sales:1254),
(day:Util.getDate(offset:4),sales:1200),
(day:Util.getDate(offset:5),sales:983),
(day:Util.getDate(offset:6),sales:1101),
(day:Util.getDate(offset:7),sales:801),
]
var body: some View {
Chart(data,id: \.day) {
BarMark(
x: .value("日期", $0.day,unit: .day),
y: .value("銷售量", $0.sales)
)
.foregroundStyle(.pink)
}
}
}
class Util {
static func getDate(offset:Int) -> Date {
let calendar = Calendar.current
return calendar.date(byAdding: .day, value: -offset, to: Date()) ?? Date()
}
}
#Preview {
ContentView()
}

如上,我們先實(shí)現(xiàn)了最近一周銷量柱狀圖,創(chuàng)建了Util工具類用于獲取過去某天的日期,然后以日期升序?yàn)閤坐標(biāo),日銷售量為y坐標(biāo)繪制出了對(duì)應(yīng)的柱狀圖。
我們現(xiàn)在想要以折線圖形式展示數(shù)據(jù),只需一個(gè)小改動(dòng),將上面代碼中的BarMark改為L(zhǎng)ineeMark即可,核心代碼塊:
var body: some View {
Chart(data,id: \.day) {
LineMark(
x: .value("日期", $0.day,unit: .day),
y: .value("銷售量", $0.sales)
)
.foregroundStyle(.pink)
}
}
預(yù)覽效果:

我們還可以在同一個(gè)圖表中,顯示廣州、深圳兩個(gè)城市最近一周的銷售對(duì)比折線圖,核心代碼如下:
struct ContentView: View {
var body: some View {
let gzData:[(day:Date,sales:Int)] = [
(day:Util.getDate(offset:1),sales:1666),
(day:Util.getDate(offset:2),sales:1899),
(day:Util.getDate(offset:3),sales:1254),
(day:Util.getDate(offset:4),sales:1200),
(day:Util.getDate(offset:5),sales:983),
(day:Util.getDate(offset:6),sales:1101),
(day:Util.getDate(offset:7),sales:801),
]
let szData:[(day:Date,sales:Int)] = [
(day:Util.getDate(offset:1),sales:2287),
(day:Util.getDate(offset:2),sales:1655),
(day:Util.getDate(offset:3),sales:1598),
(day:Util.getDate(offset:4),sales:1067),
(day:Util.getDate(offset:5),sales:900),
(day:Util.getDate(offset:6),sales:1201),
(day:Util.getDate(offset:7),sales:540),
]
let sericeData = [
(city:"廣州",data: gzData),
(city:"深圳",data: szData)
]
Chart {
ForEach(sericeData,id: \.city) { serice in
ForEach(serice.data,id: \.day) {
LineMark(
x: .value("日期", $0.day,unit: .day),
y: .value("銷售量", $0.sales)
)
}
.foregroundStyle(by: .value("City", serice.city))
}
}
}
}
預(yù)覽效果:

其中代碼
.foregroundStyle(by: .value("City", serice.city))
表示我們需要不同城市以不同顏色進(jìn)行區(qū)分。同樣的,我們還可以設(shè)置折線為平滑曲線,只需添加以下一行配置:
.interpolationMethod(.catmullRom)
預(yù)覽效果:

為了使數(shù)據(jù)更明顯,也可以給線條加上符號(hào):
.symbol(.circle)
預(yù)覽效果:

嘗試將LineMark改回BarMark,看下是什么效果?
再添加.position(by: .value("City", serice.city)),看下是什么效果?
4、其它類型
除了上面介紹了的柱狀圖和折線圖,Charts還支持其它標(biāo)記類型:

甚至能將他們相互組合,構(gòu)建更為復(fù)雜的圖表。如下圖,使用LineMark顯示某家店最近一年每個(gè)月的日均銷售量,同時(shí)通過AreaMark顯示當(dāng)月最高和最低銷售量情況:

完整代碼如下:
import SwiftUI
import Charts
struct ContentView: View {
var body: some View {
let data = [
(month:Util.getMonth(offset:1),dailyAverage:939,dailyMin: 550,daiyMax: 1209),
(month:Util.getMonth(offset:2),dailyAverage:879,dailyMin: 399,daiyMax: 1127),
(month:Util.getMonth(offset:3),dailyAverage:840,dailyMin: 422,daiyMax: 1009),
(month:Util.getMonth(offset:4),dailyAverage:823,dailyMin: 439,daiyMax: 991),
(month:Util.getMonth(offset:5),dailyAverage:797,dailyMin: 378,daiyMax: 965),
(month:Util.getMonth(offset:6),dailyAverage:800,dailyMin: 380,daiyMax: 891),
(month:Util.getMonth(offset:7),dailyAverage:820,dailyMin: 401,daiyMax: 911),
(month:Util.getMonth(offset:8),dailyAverage:832,dailyMin: 390,daiyMax: 1001),
(month:Util.getMonth(offset:9),dailyAverage:791,dailyMin: 356,daiyMax: 954),
(month:Util.getMonth(offset:10),dailyAverage:801,dailyMin: 370,daiyMax: 959),
(month:Util.getMonth(offset:11),dailyAverage:765,dailyMin: 311,daiyMax: 876),
(month:Util.getMonth(offset:12),dailyAverage:789,dailyMin: 339,daiyMax: 991),
]
Chart(data,id: \.month) {
AreaMark(
x: .value("月", $0.month, unit: .month),
yStart: .value("日最低", $0.dailyMin),
yEnd: .value("日最高", $0.daiyMax)
)
.opacity(0.3)
LineMark(
x: .value("月", $0.month, unit: .month),
y: .value("日均銷量", $0.dailyAverage)
)
}
}
}
class Util {
static func getMonth(offset:Int) -> Date {
let calendar = Calendar.current
return calendar.date(byAdding: .month, value: -offset, to: Date()) ?? Date()
}
}
#Preview {
ContentView()
}
將AreaMark和LineMark改為BarMark和RectangleMark,可以得到完全不同的圖表樣式:

核心代碼:
Chart(data,id: \.month) {
BarMark(
x: .value("月", $0.month, unit: .month),
yStart: .value("日最低", $0.dailyMin),
yEnd: .value("日最高", $0.daiyMax),
width: .ratio(0.6)
)
.opacity(0.3)
RectangleMark(
x: .value("月", $0.month, unit: .month),
y: .value("日均銷量", $0.dailyAverage),
width: .ratio(0.6),
height: 2
)
}
甚至,我們可以再添加一個(gè)RuleMark,且設(shè)置annotation屬性來顯示對(duì)應(yīng)文字 :
RuleMark(
y: .value("平均", 825)
)
.lineStyle(StrokeStyle(lineWidth: 1))
.foregroundStyle(.red)
.annotation(position: .top, alignment: .leading) {
Text("平均:825")
.font(.headline)
.foregroundStyle(.red)
}

這一篇先到這里吧,下一篇將探索Charts的自定義屬性,打造更符合我們app風(fēng)格的個(gè)性化圖表。