WWDC22- Swift Charts初探

語言: CN / TW / HK

Olaf⛄️ 2022-8-12終稿

前言

作為一個iOS開發者,我們在日常開發中,多多少少會遇到圖示展示類的需求,相信大家解決圖示需求時,都會直接使用或者參考github開源的圖表繪製框架:Charts,這個基於CoreGraphics繪製圖表的框架,支撐了相當一部分的圖表開發需求。

2022,Apple自己的圖表框架來了!

全新的 Swift Charts 框架在WWDC22中跟開發者見面了。這是一個強大而簡潔的SwiftUI框架,能將資料轉換為資訊豐富的可自定義視覺化圖表。下面是相關的4個Session:

強力建議觀看Session!!!

框架特性

上圖是Seesion中的效果呈現、展示了Swift Charts強大的繪製能力,從簡單的柱狀圖到複雜的向量圖,熱力圖,Swift Charts都可以用簡潔的程式碼實現。Swift Charts 支援 localization 本地化 和 accessibility 輔助功能。還可以使用圖表修飾符覆蓋預設行為以自定義圖表。 例如,可以通過向圖表新增動畫來建立動態體驗。

相容性

iOS 16+、iPadOS 16.0+、macOS 13.0+、watchOS 9.0+

Marks

  • BarMark

可以使用BarMark建立不同型別的條形圖,如柱狀圖,進度條圖等。 * LineMark

可以通過繪製類別或日期屬性(通常使用 x 位置)和繪製數字類別(通常使用 y 位置)來建立折線圖。 * PointMark

可以使用 PointMark 圖表內容建立不同型別的點圖表。可以使用點標記構建的一個常見圖表是散點圖,它顯示兩個數值資料屬性之間的關係。 * AreaMark

可以使用 AreaMark 將資料視覺化為單個區域形狀。 可以使用 AreaMark 圖表內容建立不同型別的面積圖。 要建立簡單的區域標記圖表,我們通常會將日期或有序字串屬性繪製到 x 位置,將數字繪製到 y 位置。 * RuleMark

可以使用 RuleMark 在圖表中繪製水平或垂直規則,如平均線,閾值線等。 * RectangleMark

可以使用RectangleMark將資料欄位對映成矩形圖。 可以使用RectangleMark建立熱圖圖表或註釋圖表中的矩形區域。

以上就是Swift Charts所有的Mark,它們可以單獨使用,也可以組合使用,從而支撐開發者按需求開發出所需要的圖表。用Session中的圖來總結這些Mark吧

框架實踐

實現Swift Charts框架,首先需要對SwiftUI的語法有一定的瞭解,下面將挑選典型圖表進行coding實踐。

BarkMark

如果我們需要用一張圖表來直觀的呈現多個國家的某一項開支費用,那我們就可以直接用BarMark來構建一個簡單的柱狀圖來實現。

``` struct BarMarkData: Identifiable { let name: String let count: Double var id: String{name} }

let defData: [BarMarkData] = [     BarMarkData(name: "China", count: 15000),     BarMarkData(name: "US", count: 30000),     BarMarkData(name: "UK", count: 2000),     BarMarkData(name: "Japan", count: 800),     BarMarkData(name: "France", count: 3000) ]

struct BarMarkChartView: View { var body: some View { Chart(data) { BarMark( x: .value("name", $0.name), y: .value("count", $0.count) ) }.frame(width: 360, height: 300) } }

struct BarMarkChartView_Previews: PreviewProvider { static var previews: some View { AreaMarkChartView() } } ```

我們看呈現效果:

如果這項開支還需要跟另一項開支進行對比呈現,這該怎麼實現呢?我們只需要將上面的程式碼略微改動,即可實現,一下只貼上新增部分的程式碼:

```Swift let eduData: [BarMarkData] = [     BarMarkData(name: "China", count: 28000),     BarMarkData(name: "US", count: 35000),     BarMarkData(name: "UK", count: 6000),     BarMarkData(name: "Japan", count: 2000),     BarMarkData(name: "France", count: 5000) ]

let mergeData = [     (outlay: "def", data: defData),     (outlay: "edu", data: eduData) ]

struct AreaMarkChartView: View {     var body: some View {         Chart(mergeData, id: .outlay) { mergeData in             ForEach(defData) { datum in                 ForEach(mergeData.data, id: .id) { element in                     BarMark(                         x: .value("name", element.name),                         y: .value("count", element.count)                     ).position(by: .value("outlay", mergeData.outlay))                     .foregroundStyle(by: .value("outlay", mergeData.outlay))                 }             }         }.frame(width: 360, height: 300)     } } ```

我們再來看看呈現效果:

現在已經實現了幾個國家兩個費用支出的對比圖。對比上面,在程式碼上除了新增資料來源,在Charts實現環節,也新增了position和foregroundStyle兩個擴充套件方法

  • .position

position可以建立分組,讓圖表沿水平軸按其“型別”標記具有相同“產品”的mark。 - .foregroundStyle

foregroundStyle按照傳參不同,可進行不同型別的展示設定,如下:

``` extension ChartContent {

/// Sets the foreground style for marks in this chart content.
///
/// - Parameter color: The color.
public func foregroundStyle<S>( _ style: S) -> some ChartContent where S : ShapeStyle


/// Encodes data as the foreground style for marks in this chart content.
///
/// - Parameter data: The data property or value.
public func foregroundStyle<D>(by value: PlottableValue<D>) -> some ChartContent where D : Plottable

} ```

LineMark

如果我們需要展示公司統計的一年內營收資料的變化,並且對比往年的資料,那此時我們可以用到的最簡單的圖就是線性圖了,當然我們也可以使用其他的型別的圖表來表示。下面我們先用LineMark來進行實踐

``` struct SalesSummary: Identifiable { let month: String let total: Int var id: String {month} }

let pastData: [SalesSummary] = [ .init(month: "Jan", total: 3388), .init(month: "Feb", total: 4420), .init(month: "Mar", total: 6120), ... .init(month: "Dec", total: 11076) ]

let lineData: [SalesSummary] = [ .init(month: "Jan", total: 5566), .init(month: "Feb", total: 4610), .init(month: "Mar", total: 5533), ... .init(month: "Dec", total: 12998) ]

let salesData = [ (quarter: "2022", data: currentData), (quarter: "2021", data: pastData) ]

struct LineMarkChartView: View { var body: some View { Chart{ ForEach(salesData, id: .years) { salesData in
ForEach(salesData.data, id: .id) { element in LineMark(x: .value("month", element.month), y: .value("total", element.total)) }.foregroundStyle(by: .value("sales", salesData.years)) } }.frame(width: 360, height: 300) } }

struct LineMarkChartView_Previews: PreviewProvider { static var previews: some View { LineMarkChartView() } } ```

我們來看效果:

如果想給折線圖增加一個數據點的標記符號,同時讓折線變的圓潤,那我們就需要實現以下兩個屬性

struct LineMarkChartView: View { var body: some View { Chart{ ForEach(salesData, id: .years) { salesData in ForEach(salesData.data, id: .id) { element in LineMark(x: .value("month", element.month), y: .value("total", element.total)) .interpolationMethod(.catmullRom) .symbol(by: .value("sales", salesData.years)) }.foregroundStyle(by: .value("sales", salesData.years)) } }.frame(width: 360, height: 300) } }

再看下效果:

此時我們可以看到資料節點都用原點標記出來,並且折線也變成了更加柔和的曲線,這就是 interpolationMethod 和 symbol 這兩個屬性發揮的作用。

  • interpolationMethod

返回使用給定插值繪製資料的圖表內容,包含多個,這裡不一一列舉了,可以檢視api,這個方法僅提供給Line 和 Area 兩種Mark - symbol

symbol根據傳參不同有三個不同的方法實現,來支援不同的特性功能

```Swift extension ChartContent { /// 設定此圖表內容中標記的繪圖符號型別 /// - Parameter symbol: The symbol. public func symbol( _ symbol: S) -> some ChartContent where S : ChartSymbolShape

/// 將資料編碼為此圖表內容中標記的符號。如上面的例子
/// - Parameter data: The data property or value.
public func symbol<D>(by value: PlottableValue<D>) -> some ChartContent where D : Plottable

/// 返回以給定檢視作為繪圖符號的圖表內容。
/// - Parameter symbol: The view to use as the plotting symbol.
public func symbol<V>(@ViewBuilder symbol: () -> V) -> some ChartContent where V : View

} ```

AreaMark

上面的公司年度營收的折線圖資料同樣也可以用來實踐AreaMark區域圖,我們來看看具體的程式碼實現,會不會更復雜呢,還是同樣簡單

``` let areaData: [BarMarkData] = [ .init(month: "Jan", total: 5566), .init(month: "Feb", total: 4610), .init(month: "Mar", total: 5533), ... .init(month: "Dec", total: 12998) ]

struct AreaMarkChartView: View { var body: some View { Chart(areaData) { element in
AreaMark( x: .value("month", element.month), y: .value("total", element.total) ).interpolationMethod(.catmullRom) .foregroundStyle(.pink) }.frame(width: 360, height: 300) } } ```

同樣我們先來看看效果:

同樣,AreaMark也使用了interpolationMethod來增加了邊緣的曲線,同時還使用foregroundStyle來設定區域的顏色。

更多支援

Swift Charts可以三種資料型別作為他的資料值

  • Quantitative(資料型,如果Int、Double等)

  • Nominal(名義型,如各類名稱標籤)

  • Temporal(時間型,如年月週日時分等)

所以,6種Mark實際只有三種資料類別,每種Mark通常使用的屬性通產個包括以下這些,這些屬性,我們再上面的實踐Demo中也都有使用到:

那麼這些屬性是不是足夠了呢?

當然,是不夠的。我們在實際開發中呈現的UI需求往往是定製化元素比較多的,現在我們來看看Swift Charts 框架都能夠支援哪些內容的自定義,Seesion中給我們羅列了Chart的自定義內容維度:

接下來,我從途中所列的自定義屬性入手,結合上面的的Demo例子,進一步實踐Swift Charts的自定義能力。

增加Plot屬性後,我們看看實際的效果

``` .chartPlotStyle { plot in plot.background(.purple.opacity(0.8)) .border(.purple, width: 2) .frame(width: 150, height: 100) } // 此時的效果,下圖左。 // 如果將這個圖示展示在Apple Watch,或者Widget上, // 座標線的效果此時會成為一種干擾,當然我們可以隱藏座標線 .chartXAxis(.hidden) .chartYAxis(.hidden) // 此時的效果,下圖右

```

當然,關於這三個屬性的應用,不止於此。它們都有很強大的自定義支撐能力來應對圖示的展示需求,再結合Descriptions、Interaction、Color來實現更加酷炫的圖表呈現。

關於使用

Charts的圖表呈現能力雖然強大,但是我們都知道圖表有它特殊的呈現場景,那麼何時應該較互動呈現為圖表,蘋果的Session中已經給我們進行的設計指引。

當我們需要將資料進行變化、對比、進度(比例)呈現時,我們就可以使用Chart來幫我們進行視覺化的實現 了。

如上三張圖所示:

  • 當我們需要視覺化某項持續性資料變化趨勢時,我們就可以使用Change部分的視覺化方式來呈現。例如:使用者單日步數、使用者消費、瀏覽時長等資料變化趨勢。

  • 當我們需要視覺化某項任務的進度,完成度、參與度、佔比等等資料時,我們就可以使用Proportion部分的視覺化形式來呈現。例如:使用者年度App各類業務消費佔比。

  • 當我們需要視覺化更全面資料的對比及跟維度的資料時,我們可以使用Comparison部分的視覺化形式來呈現。如各公司的資料平臺等系統的資料。

綜上,Charts的使用場景,可能依然是眾多App中的小部分,但是在特定行業App的業務中則是核心能力的展示,如你需要做一個股票或者財經類的App,又或者關於健康資料的App。Apple Watch的應用中則對Swift Charts有更多的需求。下面我們可以看看Session中的一些案例。

寫在最後

Swift Charts是在WWDC22上和大家見面的,其強大的圖表呈現能力,隨著你對框架的挖掘和熟悉,會逐漸感知到。那麼當你看了這篇文章後,你對Swift Charts有什麼想法?

  • 好傢伙,相容性最低iOS16,哪年那月才能用上...

  • 基於SwiftUI,我還不如用danielgindi/Charts,畢竟SwiftUI也得最低iOS13的支援,等到那時候再看SwiftUI吧...

這些問題確確實實存在,而且當你熟悉Swift Charts後,也一定會發現有它所不能支撐的需求場景。Swift Charts在WWDC22和大家見面,我相信在WWDC23/24或者更遠的將來,Swift Charts框架也會持續的更新和改進,有Widget Kit開發需求,或者Apple Watch開發需求的小夥伴,建議持續保持關注。

參考文獻