###### tags: `第14屆IT邦鐵人賽文章` # 【在 iOS 開發路上的大小事2-Day29】來自 Apple 爸爸的最新力作 - Swift Charts 之 BarMark 實作篇 上一篇介紹了 RuleMark 的實作,今天要來介紹的是 Swift Charts 系列中的最後一個圖表 BarMark BarMark 一共提供了七種 init 的方法,讓開發者可以繪製不同樣式的圖表 ```swift public init<X, Y>(x: PlottableValue<X>, y: PlottableValue<Y>, width: MarkDimension = .automatic, height: MarkDimension = .automatic, stacking: MarkStackingMethod = .standard) where X : Plottable, Y : Plottable public init<X>(x: PlottableValue<X>, yStart: CGFloat? = nil, yEnd: CGFloat? = nil, width: MarkDimension = .automatic, stacking: MarkStackingMethod = .standard) where X : Plottable public init<Y>(xStart: CGFloat? = nil, xEnd: CGFloat? = nil, y: PlottableValue<Y>, height: MarkDimension = .automatic, stacking: MarkStackingMethod = .standard) where Y : Plottable public init<X, Y>(xStart: PlottableValue<X>, xEnd: PlottableValue<X>, y: PlottableValue<Y>, height: MarkDimension = .automatic) where X : Plottable, Y : Plottable public init<X>(xStart: PlottableValue<X>, xEnd: PlottableValue<X>, yStart: CGFloat? = nil, yEnd: CGFloat? = nil) where X : Plottable public init<X, Y>(x: PlottableValue<X>, yStart: PlottableValue<Y>, yEnd: PlottableValue<Y>, width: MarkDimension = .automatic) where X : Plottable, Y : Plottable public init<Y>(xStart: CGFloat? = nil, xEnd: CGFloat? = nil, yStart: PlottableValue<Y>, yEnd: PlottableValue<Y>) where Y : Plottable ``` ## Model ```swift import SwiftUI struct DepartmentEntity: Identifiable { var id = UUID().uuidString var department: String var profit: Int } ``` ## ViewModel ```swift import SwiftUI class DepartmentEntityViewModel { var departmentData: [DepartmentEntity] = [ .init(department: "Production", profit: 15000), .init(department: "Marketing", profit: 8000), .init(department: "Finance", profit: 10000) ] } ``` ## View 這邊要記得 ```import Charts```,因為我們要顯示 BarMark 在畫面上 然後這邊宣告了一個 ViewModel 的變數 deVM 並在前面加上 ```@State``` 修飾字,讓 SwiftUI 來幫我們管理 ViewModel 狀態 接著是 Charts 的語法,語法也是很簡單,像是下面這樣 ```swift @State private var deVM = DepartmentEntityViewModel() // 1:deVM.departmentData,圖表的資料來源 Chart(deVM.departmentData) { BarMark( x: .value("Department", $0.department), // 2:x 軸要顯示的資料 y: .value("Profit", $0.profit) // 3:y 軸要顯示的資料 ) } .frame(height: 300) .padding() ``` 或者你也可以透過 ForEach 來寫,只是就會要讓 Model 繼承 Identifiable 並宣告 UUID() 變數在 Model 裡面,像是這樣 ```var id = UUID().uuidString``` ```swift @State private var deVM = DepartmentEntityViewModel() Chart { // 1:deVM.departmentData,圖表的資料來源 ForEach(deVM.departmentData) { department in BarMark( x: .value("Department", department.department), // 2:x 軸要顯示的資料 y: .value("Profit", department.profit) // 3:y 軸要顯示的資料 ) } } .frame(height: 300) .padding() ``` 現在的圖,應該會長得像下面這樣 ![BarMark](https://i.imgur.com/vghtGfc.png) 如果要將每個 Bar 都顯示對應數值的話,可以透過 ```.annotation``` 這個 modifier ```swift @State private var deVM = DepartmentEntityViewModel() Chart { ForEach(deVM.departmentData) { department in BarMark( x: .value("Department", department.department), y: .value("Profit", department.profit) ) .annotation { Text("\(department.profit)") } } } .chartYAxisLabel("Normal", alignment: .center) .frame(height: 300) .padding() ``` 加完後,會長得像下面這樣 ![BarMark + annotation modifier](https://i.imgur.com/qYUDfxr.png) ## 堆疊樣式的 BarMark 接下來還有像是堆疊樣式的 BarMark 讓我們先來改寫一下 ### Model ```swift import SwiftUI struct DepartmentCategoryEntity: Identifiable { var id = UUID().uuidString var department: String var profit: Double var category: String } ``` ### ViewModel ```swift import SwiftUI class DepartmentCategoryEntityViewModel { var departmentData: [DepartmentCategoryEntity] = [ .init(department: "Production", profit: 4000, category: "Gizmos"), .init(department: "Production", profit: 5000, category: "Gadgets"), .init(department: "Production", profit: 6000, category: "Widgets"), .init(department: "Marketing", profit: 2000, category: "Gizmos"), .init(department: "Marketing", profit: 1000, category: "Gadgets"), .init(department: "Marketing", profit: 5000, category: "Widgets"), .init(department: "Finance", profit: 2000, category: "Gizmos"), .init(department: "Finance", profit: 3000, category: "Gadgets"), .init(department: "Finance", profit: 5000, category: "Widgets") ] } ``` ### View ```swift @State private var dceVM = DepartmentCategoryEntityViewModel() Chart { ForEach(dceVM.departmentData) { department in BarMark( x: .value("Category", department.department), y: .value("Profit", department.profit), stacking: .standard ) .foregroundStyle(by: .value("Product Category", department.category)) } } .chartYAxisLabel("Stacking.standard", alignment: .center) .frame(height: 300) .padding() ``` 這邊有一個 optional 參數 ```stacking:``` 可以改變 BarMark 的堆疊樣式 stacking 樣式一共有四種,```standard (預設值)```、```normalized```、```center```、```unstacked``` 可以依照自己的需求,來改變 BarMark 的顯示方式 ```swift @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) @frozen public struct MarkStackingMethod : Equatable { /// Stack marks starting at zero. /// /// Negative values appear below zero, creating diverging stacked marks. @inlinable public static var standard: MarkStackingMethod { get } /// Create normalized stacked bar and area charts. @inlinable public static var normalized: MarkStackingMethod { get } /// Stack marks using a center offset. /// /// Use this type to create a stream graph. @inlinable public static var center: MarkStackingMethod { get } /// Don't stack marks. @inlinable public static var unstacked: MarkStackingMethod { get } } ``` ## 一維樣式的 BarMark 在 手機設定 -> 一般 -> iPhone 儲存空間 裡面,會看到最上面有一條柱狀圖 ![iPhone 儲存空間的柱狀圖](https://i.imgur.com/70rrOAN.png) 那我們要如何繪製一個類似的圖表呢?這時候就可以透過 BarMark 不同的 init 來做到 ```swift public init<X>(x: PlottableValue<X>, yStart: CGFloat? = nil, yEnd: CGFloat? = nil, width: MarkDimension = .automatic, stacking: MarkStackingMethod = .standard) where X : Plottable ``` ### Model ```swift import SwiftUI struct FileCategoryEntity: Identifiable { var id = UUID().uuidString var fileSizePercent: Double var fileCategory: String } ``` ### ViewModel ```swift import SwiftUI class FileCategoryEntityViewModel { var fileData: [FileCategoryEntity] = [ .init(fileSizePercent: 20, fileCategory: "App"), .init(fileSizePercent: 40, fileCategory: "照片"), .init(fileSizePercent: 5, fileCategory: "媒體"), .init(fileSizePercent: 10, fileCategory: "訊息"), .init(fileSizePercent: 12, fileCategory: "iOS"), .init(fileSizePercent: 13, fileCategory: "系統資料"), ] } ``` ### View ```swift @State private var vm = FileCategoryEntityViewModel() Chart { ForEach(vm.fileData) { file in BarMark( x: .value("File Size Percent", file.fileSizePercent) ) .foregroundStyle(by: .value("File Category", file.fileCategory)) } } .chartXAxis(.hidden) .frame(height: 100) .padding() ``` 現在的圖,應該會長得像下面這樣 ![1D BarMark](https://i.imgur.com/4VyCeEg.png) ## 完整程式碼 (BarMark) ```swift import SwiftUI import Charts struct BarMarkView: View { @State private var deVM = DepartmentEntityViewModel() var body: some View { Chart { ForEach(deVM.departmentData) { department in BarMark( x: .value("Department", department.department), y: .value("Profit", department.profit) ) .annotation { Text("\(department.profit)") } } } .chartYAxisLabel("Normal", alignment: .center) .frame(height: 300) .padding() } } struct BarChartView_Previews: PreviewProvider { static var previews: some View { BarMarkView() } } ``` ## 完整程式碼 (堆疊樣式的 BarMark) ```swift import SwiftUI import Charts struct BarMarkView: View { @State private var dceVM = DepartmentCategoryEntityViewModel() var body: some View { Chart { ForEach(dceVM.departmentData) { department in BarMark( x: .value("Category", department.department), y: .value("Profit", department.profit), stacking: .standard ) .foregroundStyle(by: .value("Product Category", department.category)) } } .chartYAxisLabel("Stacking.standard", alignment: .center) .frame(height: 300) .padding() } } struct BarChartView_Previews: PreviewProvider { static var previews: some View { BarMarkView() } } ``` ## 完整程式碼 (一維樣式的 BarMark) ```swift import SwiftUI import Charts struct OneDBarMarkView: View { @State private var vm = FileCategoryEntityViewModel() var body: some View { Chart { ForEach(vm.fileData) { file in BarMark( x: .value("File Size Percent", file.fileSizePercent) ) .foregroundStyle(by: .value("File Category", file.fileCategory)) } } .chartXAxis(.hidden) .frame(height: 100) .padding() } } struct OneDBarMarkView_Previews: PreviewProvider { static var previews: some View { OneDBarMarkView() } } ``` ## 總結 這篇簡單實作了 Swift Charts 中的 BarMark 在這幾篇的 Swift Charts 實作中,我個人覺得 Swift Charts 算是滿容易上手的,功能也算多 唯一美中不足的部分可能就是只支援 SwiftUI,但現在 UIKit 也可以透過 UIHostingController 來串接 SwiftUI 的畫面,所以說還可以啦😂 期待之後 Apple 為 Swift Charts 加入更多可玩性! 在這幾篇所實作的 Swift Charts 的完整程式碼,可以到我的 [GitHub](https://github.com/leoho0722/SwiftChartsDemo) 上找到喔~ ## 參考資料 > 1. [https://developer.apple.com/documentation/charts/barmark](https://developer.apple.com/documentation/charts/barmark)