精通 SwiftUI - iOS 17 版

第 48 章
建立圓餅圖和甜甜圈圖

圓餅圖(Pie charts)和甜甜圈圖(Donut charts)是在數據可視化中常用的兩種圖表類型。在 iOS 17 之前,如果你想使用 SwiftUI 創建這些類型的圖表,就需要使用 PathArc 等組件自己構建圖表。在第8章中,我們展示了如何從頭開始實現圓餅圖和甜甜圈圖的技巧。然而,自從 iOS 17 發布以來,這已經不再必要了。SwiftUI 通過引入一種名為 SectorMark 的新標記類型,簡化了創建這些圖表的過程。這使得開發人員可以輕鬆構建各種類型的圓餅圖和甜甜圈圖。

在本章中,我們將指導您使用 SwiftUI 構建圓餅圖和甜甜圈圖的過程。除此之外,你還會了解如何為這些圖表添加交互功能。

重温長條圖(Bar Charts)

讓我們從使用 Charts 框架實現一個簡單的長條圖開始。假設你已經創建了一個新的 SwiftUI 項目,在下面插入程式碼以初始化柱狀圖的示例數據:

private var coffeeSales = [
    (name: "Americano", count: 120),
    (name: "Cappuccino", count: 234),
    (name: "Espresso", count: 62),
    (name: "Latte", count: 625),
    (name: "Mocha", count: 320),
    (name: "Affogato", count: 50)
]

這些只是用於圖表渲染的隨機咖啡銷售數據。為了簡單起見,我使用了一個元組數組來保存數據。Charts 框架使得開發人員可以非常輕鬆地從這些數據創建長條圖。

首先,導入 Charts 框架並將 body 部分替換為以下程式碼:

VStack {
    Chart {
        ForEach(coffeeSales, id: \.name) { coffee in
            BarMark(
                x: .value("Type", coffee.name),
                y: .value("Cup", coffee.count)
            )
            .foregroundStyle(by: .value("Type", coffee.name))
        }
    }
}
.padding()

無論你創建長條圖還是圓餅圖,一切都始於 Chart 視圖。在這個視圖中,我們定義了一組 BarMark,用於在 x 軸上繪製咖啡類型,在 y 軸上繪製數量的垂直長條圖。foregroundStyle 修飾器會自動為每個長條圖分配一個獨特的顏色。

圖 48.1. 長條圖
圖 48.1. 長條圖

你可以通過修改一些 BarMark 參數輕鬆創建不同類型的長條圖。

圖 48.2. 1-D 長條圖
圖 48.2. 1-D 長條圖

例如,如果你想要建立一個一維長條圖,只需要提供 x 軸或 y 軸的值即可:

VStack {
    Chart {
        ForEach(coffeeSales, id: \.name) { coffee in

            BarMark(
                x: .value("Cup", coffee.count)
            )
            .foregroundStyle(by: .value("Type", coffee.name))
        }
    }
    .frame(height: 100)
}
.padding()

在預設的情況下,它在 x 軸上顯示累計計數。如果你想要對值進行歸一化,只需為 BarMark 指定 stacking 參數,如下所示:

BarMark(
    x: .value("Cup", coffee.count),
    stacking: .normalized
)
.foregroundStyle(by: .value("Type", coffee.name))

使用 SectorMark 建立圓餅圖

現在我們已經建立了一個長條圖,讓我們看看如何使用 iOS 17 中引入的新的 SectorMark 將其轉換為圓餅圖。

正如其名稱所示,SectorMark 表示圓餅圖中對應於特定類別的扇區。每個 SectorMark 由其表示的值來定義。通過使用 SectorMark,開發人員可以輕鬆創建各種類型的圓餅圖(或甜甜圈圖),而無需從頭開始使用 PathArc 等組件構建它們。

例如,如果我們想將長條圖轉換為圓餅圖,就只需要將 BarMark 替換為 SectorMark,如下所示:

Chart {
    ForEach(coffeeSales, id: \.name) { coffee in

        SectorMark(
            angle: .value("Cup", coffee.count)
        )
        .foregroundStyle(by: .value("Type", coffee.name))
    }
}
.frame(height: 500)

與指定 x 軸的值不同,你將值傳遞給 angle 參數。SwiftUI 將自動計算扇區的角度大小並生成圓餅圖。

圖 48.3. 使用 SectorMark 建立圓餅圖
圖 48.3. 使用 SectorMark 建立圓餅圖

自定義圓餅圖

SectorMark 提供了一些參數,供你自定義每個扇區。要在扇區之間添加一些間距,你可以提供 angularInset 的值。

圖 48.4. 使用 angularInset 在扇區之間添加一些間距
圖 48.4. 使用 angularInset 在扇區之間添加一些間距

你可以通過為 outerRadius 參數指定值來控制扇區的大小。例如,如果你想要通過增大 Latte 扇區來突出顯示它,就可以添加 outerRadius 參數。

圖 48.5. 放大其中一個扇區
圖 48.5. 放大其中一個扇區

如要為每個扇區添加標籤,你可以將 annotation 修飾器附加到 SectorMark 上,並將 position 設置為 .overlay

.annotation(position: .overlay) {
    Text("\(coffee.count)")
        .font(.headline)
        .foregroundStyle(.white)
}

我們只是在每個扇區上添加了一個文本標籤,用於顯示數量。

圖 48.6. 為每個扇區上添加文本標籤
圖 48.6. 為每個扇區上添加文本標籤

將圓餅圖轉換為甜甜圈圖

那麼,如何建立甜甜圈圖呢? 新的「SectorMark」功能非常強大,你只需添加一行程式碼即可將圓餅圖變成甜甜圈圖。 SectorMark 有一個我之前沒有提到的可選參數。

要建立甜甜圈圖,只需指定 SectorMark 的 innerRadius 參數並將其傳遞你的首選值:

SectorMark(
    angle: .value("Cup", coffee.count),
    innerRadius: .ratio(0.65),
    angularInset: 2.0
)

innerRadius 的值要麼是以點為單位的大小,要麼是相對於外半徑的.ratio.inset。 如數值大於零,就可以在圓餅圖中建立一個洞,並將圖表變成甜甜圈圖。

圖 48.7. 利用 innerRadius 將圓餅圖轉換為甜甜圈圖
圖 48.7. 利用 innerRadius 將圓餅圖轉換為甜甜圈圖

另外,你可以將 cornerRadius 修改器附加到扇區標記以圓化扇區的角落。

圖 48.8. 使用cornerRadius來圓化扇區的角
圖 48.8. 使用cornerRadius來圓化扇區的角

你也可以將 chartBackground 修飾器附加到 Chart 視圖來將任何視圖新增至圖表的背景。 以下是一個例子。

圖 48.9. 設定圖表的背景
圖 48.9. 設定圖表的背景

與圖表互動

除了引入 SectorMark 之外,新版本的 SwiftUI 還提供了新的圖表 API,用於處理使用者互動。 對於餅圖和甜甜圈圖,你可以附加 chartAngleSelection 修飾器並向其傳遞一個綁定以捕獲使用者的觸摸:

@State private var selectedCount: Int?

Chart {

    .
    .
    .

}
.chartAngleSelection(value: $selectedCount)

chartAngleSelection 修飾器接受與可繪製值的綁定。 由於我們所有的可繪圖值都是整數,因此我們聲明一個 Int 類型的狀態變數。 透過這個實作,圖表現在可以偵測使用者的觸控並捕獲甜甜圈圖(或圓餅圖)的選定數值。

圖 48.10. 偵測使用者的觸控
圖 48.10. 偵測使用者的觸控

你可以將 onChange 修飾器附加到圖表以顯示所選值。

.onChange(of: selectedCount) { oldValue, newValue in
    if let newValue {
        print(newValue)
    }
}

偵測到的值並不會直接告訴你使用者觸摸的確切扇區。 相反,它給出了所選咖啡濃度的值。 例如,如果使用者點擊綠色扇區的後緣,SwiftUI 會傳回值「354」。

圖 48.11. 了解偵測到的值
圖 48.11. 了解偵測到的值

要從所偵測到的值找出相關的區,我們需要建立一個新函數。 此函數採用選定的值並傳回對應區域的名稱。

private func findSelectedSector(value: Int) -> String? {

    var accumulatedCount = 0

    let coffee = coffeeSales.first { (_, count) in
        accumulatedCount += count
        return value <= accumulatedCount
    }

    return coffee?.name
}

透過上面的實作,我們可以聲明一個狀態變數來保存選定的扇區並對甜甜圈圖進行一些更改。

@State private var selectedSector: String?

當選擇圖表的一個扇區時,我們會將其餘扇區變暗,同時以反白所選扇區。 像這樣更新onChange修飾器:

.onChange(of: selectedCount) { oldValue, newValue in
    if let newValue {
        selectedSector = findSelectedSector(value: newValue)
    } else {
        selectedSector = nil
    }
}

然後,將 opacity 修飾器附加到 SectorMark,如下所示:

SectorMark {

...

}
.opacity(selectedSector == nil ? 1.0 : (selectedSector == coffee.name ? 1.0 : 0.5))

當沒有選定的扇區時,我們保持原始的不透明度。 一旦使用者觸摸特定區域,我們就會更改那些未選定區域的不透明度。 下面顯示了選擇 Latte 扇區時圓環圖的外觀。

圖 48.12. 觸碰某個部分時更改不透明度
圖 48.12. 觸碰某個部分時更改不透明度

總結

在本教學中,我們介紹了使用 SwiftUI 建立圓餅圖和圓環圖的過程。 在 iOS 17 之前,如果你想使用 SwiftUI 建立這些類型的圖表,則必須使用「Path」等元件自行建立圖表。 然而,隨著名為「SectorMark」的新圖表 API 的引入,現在創建各種餅圖和圓環圖比以往任何時候都更容易。 正如所看到的,將長條圖變成圓餅圖(或甜甜圈圖),都只需要一些簡單的更改。

我們還討論了如何在圖表中添加互動性。 這是 SwiftUI Charts 框架的另一個新功能。 只需幾行程式碼,就可以偵測使用者的觸控並突出顯示圖表的特定部分。 我希望你喜歡閱讀本章,並開始使用 iOS 17 中提供的所有新功能來建立出色的圖表。

你可以在此處下載演示項目: