上一篇介紹了 RuleMark 的實作,今天要來介紹的是 Swift Charts 系列中的最後一個圖表 BarMark
BarMark 一共提供了七種 init 的方法,讓開發者可以繪製不同樣式的圖表
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
import SwiftUI
struct DepartmentEntity: Identifiable {
    
    var id = UUID().uuidString
    
    var department: String
    
    var profit: Int
}
import SwiftUI
class DepartmentEntityViewModel {
    
    var departmentData: [DepartmentEntity] = [
        .init(department: "Production", profit: 15000),
        .init(department: "Marketing", profit: 8000),
        .init(department: "Finance", profit: 10000)
    ]
}
這邊要記得 import Charts,因為我們要顯示 BarMark 在畫面上
然後這邊宣告了一個 ViewModel 的變數 deVM
並在前面加上 @State 修飾字,讓 SwiftUI 來幫我們管理 ViewModel 狀態
接著是 Charts 的語法,語法也是很簡單,像是下面這樣
@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
@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()
現在的圖,應該會長得像下面這樣

如果要將每個 Bar 都顯示對應數值的話,可以透過 .annotation 這個 modifier
@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
讓我們先來改寫一下
import SwiftUI
struct DepartmentCategoryEntity: Identifiable {
    
    var id = UUID().uuidString
    
    var department: String
    
    var profit: Double
    
    var category: String
}
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")
    ]
}
@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 的顯示方式
@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 }
}
在 手機設定 -> 一般 -> iPhone 儲存空間 裡面,會看到最上面有一條柱狀圖

那我們要如何繪製一個類似的圖表呢?這時候就可以透過 BarMark 不同的 init 來做到
public init<X>(x: PlottableValue<X>, 
               yStart: CGFloat? = nil, 
               yEnd: CGFloat? = nil, 
               width: MarkDimension = .automatic, 
               stacking: MarkStackingMethod = .standard) where X : Plottable
import SwiftUI
struct FileCategoryEntity: Identifiable {
    
    var id = UUID().uuidString
    
    var fileSizePercent: Double
    
    var fileCategory: String
}
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: "系統資料"),
    ]
}
@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()
現在的圖,應該會長得像下面這樣

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()
    }
}
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()
    }
}
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 上找到喔~