精通 SwiftUI - iOS 17 版

第 38 章
利用 Charts 框架建立圖表

你不再需要建立自己的圖表庫或依賴第三方庫來建立圖表。 SwiftUI 框架現在附帶圖表 API。 借助 iOS 16 或更高版本中提供的這個圖錶框架,只需幾行程式碼即可呈現動畫圖表。

建立簡單的長條圖

簡單來說,我們只需要定義 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 視圖都會有 xy 值,x 值就是代表 x 軸的圖表資料,如此類推。在以上的程式碼中,我把 x 軸的標籤設置為 Day,而 y 軸就是總步數。

讓我們在 Xcode 輸入以上程式碼,預覽就會自動顯示有兩個垂直長方體的長條圖。

圖 38.1. 基本的長條圖
圖 38.1. 基本的長條圖

以上就是創建長條圖最簡單的方法。不過,我們通常都不會對圖表數據進行硬編碼 (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]))
            }
        }
    }
}

我們為圖表數據創建了兩個陣列(weekdayssteps)。 在 Chart 視圖中,我們讀取 weekdays 陣列並顯示圖表數據。 如果你已在 Xcode 項目中輸入程式碼,預覽部分應該呈現如圖 38.2 所示的條形圖。

圖 38.2. 利用陣列儲存圖表數據
圖 38.2. 利用陣列儲存圖表數據

在預設情況下,Charts API 會以相同顏色呈現所有長方體。如果我們想把每個長方體設置為不同的顏色,可以將 foregroundStyle 修飾符附加到 BarMark 視圖:

.foregroundStyle(by: .value("Day", weekdays[index]))

如果我們想為所有長方體添加註釋,可以使用 annotation 修飾器:

.annotation {
    Text("\(steps[index])")
}

作出這些改動後,長條圖就更加漂亮了。

圖 38.3. 加入不同顏色的長條圖
圖 38.3. 加入不同顏色的長條圖

如果想要建立橫向的長條圖,我們只需要把 BarMark 視圖內的 xy 參數 (parameter) 交換就可以了。

圖 38.4. 橫向的長條圖
圖 38.4. 橫向的長條圖

建立折線圖

你已學會如何建立長條圖,現在我們會示範使用 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 的折線圖:

圖 38.5. 一個簡單的折線圖
圖 38.5. 一個簡單的折線圖

客製化圖表軸

我們可以利用 chartXAxischartYAxis 修飾符,來客製化 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。

圖 38.6. 自定義圖表軸
圖 38.6. 自定義圖表軸

客製化繪圖區域的背景顏色

我們可以利用 chartPlotStyle 修飾器,來更改繪圖區域的背景顏色。讓我們將修飾符附加到 Chart 視圖:

.chartPlotStyle { plotArea in
    plotArea
        .background(.blue.opacity(0.1))
}

然後,我們就可以使用 background 修飾符更改繪圖區域的顏色。在上面的例子中,我們把繪圖區域更改成淺藍色。

圖 38.7. 自定圖表的背景顏色
圖 38.7. 自定圖表的背景顏色

構建多於一條線的折線圖

現在,圖表只顯示單一數據(香港的天氣數據),那我們如何把倫敦和台北的天氣數據顯示在同一個折線圖中呢?

我們可以這樣重寫 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 會自動選擇顏色。

圖 38.8. 構建多個數組的折線圖
圖 38.8. 構建多個數組的折線圖

現在,三個城市的符號都相同。如果要使用不同的符號,讓我們在 foregroundStyle 之後添加這行程式碼:

.symbol(by: .value("City", series.city))

如此一來,不同城市就會有不同的符號了。

圖 38.9. 加入不同的符號
圖 38.9. 加入不同的符號

客製化內插 (Interpolation) 方法

我們可以把 interpolationMethod 修飾器附加到 LineMark,來更改折線圖的內插方法。

.interpolationMethod(.stepStart)

如果我們把內插方法設置為 .stepStart,折線圖就會變成這樣:

圖 38.10. 客製化內插 (Interpolation) 方法
圖 38.10. 客製化內插 (Interpolation) 方法

除了 .stepStart 之外,我們還可以使用以下設定:

  • cardinal
  • catmullRom
  • linear
  • monotone
  • stepCenter
  • stepEnd

總結

Charts 框架是 SwiftUI 一個很好的新功能,即使是 SwiftUI 的初學者,用幾行程式碼,就可以構建出漂亮的圖表。雖然這篇教學文章以折線圖為例子,但其實我們可以利用 Charts API 輕鬆地將折線圖轉換為其他圖表,例如長條圖。你可以參閱 Swift Charts 文檔深入了解這個 API。

在本章所準備的範例檔中,有最後完整的 Xcode 專案,可供你下載參考: