###### tags: `第14屆IT邦鐵人賽文章` # 【在 iOS 開發路上的大小事2-Day24】來自 Apple 爸爸的最新力作 - Swift Charts 之 AreaMark 實作篇 在上一篇簡單介紹了 Swift Charts ,從這篇開始,透過 6 天實作,讓你快速上手 今天要來實作的是 Swift Charts 的 AreaMark AreaMark 一共提供了六種 init 的方法,讓開發者可以繪製不同樣式的圖表 ```swift public init<X, Y>(x: PlottableValue<X>, y: PlottableValue<Y>, stacking: MarkStackingMethod = .standard) where X : Plottable, Y : Plottable public init<X, Y>(xStart: PlottableValue<X>, xEnd: PlottableValue<X>, y: PlottableValue<Y>) where X : Plottable, Y : Plottable public init<X, Y>(x: PlottableValue<X>, yStart: PlottableValue<Y>, yEnd: PlottableValue<Y>) where X : Plottable, Y : Plottable public init<X, Y, S>(x: PlottableValue<X>, y: PlottableValue<Y>, series: PlottableValue<S>, stacking: MarkStackingMethod = .standard) where X : Plottable, Y : Plottable, S : Plottable public init<X, Y, S>(xStart: PlottableValue<X>, xEnd: PlottableValue<X>, y: PlottableValue<Y>, series: PlottableValue<S>) where X : Plottable, Y : Plottable, S : Plottable public init<X, Y, S>(x: PlottableValue<X>, yStart: PlottableValue<Y>, yEnd: PlottableValue<Y>, series: PlottableValue<S>) where X : Plottable, Y : Plottable, S : Plottable ``` 我們就以護國神山台積電的股票收盤價來當作範例 ## Model ```swift struct StockPrice: Identifiable { var id = UUID().uuidString let name: String // 公司名稱 let highestPrice: Double // 當日最高點 let lowestPrice: Double // 當日最低點 let endPrice: Double // 當日收盤價 let date: Date // 日期 init(name: String, highestPrice: Double, lowestPrice: Double, endPrice: Double, month: Int, day: Int) { self.name = name self.highestPrice = highestPrice self.lowestPrice = lowestPrice self.endPrice = endPrice let calender = Calendar.autoupdatingCurrent self.date = calender.date(from: DateComponents(month: month, day: day))! } } ``` ## ViewModel 我們這邊使用 8/19~9/8 這段期間的收盤價,來當作我們的圖表資料 ```swift class StockPriceViewModel { var stockData: [StockPrice] = [ // MARK: TSMC Stock Price StockPrice(name: "TSMC", highestPrice: 523.00, lowestPrice: 517.00, endPrice: 519.00, month: 8, day: 19), StockPrice(name: "TSMC", highestPrice: 514.00, lowestPrice: 510.00, endPrice: 510.00, month: 8, day: 22), StockPrice(name: "TSMC", highestPrice: 506.00, lowestPrice: 502.00, endPrice: 504.00, month: 8, day: 23), StockPrice(name: "TSMC", highestPrice: 508.00, lowestPrice: 503.00, endPrice: 503.00, month: 8, day: 24), StockPrice(name: "TSMC", highestPrice: 510.00, lowestPrice: 504.00, endPrice: 508.00, month: 8, day: 25), StockPrice(name: "TSMC", highestPrice: 515.00, lowestPrice: 511.00, endPrice: 512.00, month: 8, day: 26), StockPrice(name: "TSMC", highestPrice: 502.00, lowestPrice: 496.00, endPrice: 498.50, month: 8, day: 29), StockPrice(name: "TSMC", highestPrice: 500.00, lowestPrice: 496.00, endPrice: 496.00, month: 8, day: 30), StockPrice(name: "TSMC", highestPrice: 505.00, lowestPrice: 492.00, endPrice: 505.00, month: 8, day: 31), StockPrice(name: "TSMC", highestPrice: 495.50, lowestPrice: 490.00, endPrice: 490.50, month: 9, day: 1), StockPrice(name: "TSMC", highestPrice: 489.50, lowestPrice: 485.00, endPrice: 485.00, month: 9, day: 2), StockPrice(name: "TSMC", highestPrice: 488.00, lowestPrice: 484.00, endPrice: 486.00, month: 9, day: 5), StockPrice(name: "TSMC", highestPrice: 491.50, lowestPrice: 486.50, endPrice: 489.00, month: 9, day: 6), StockPrice(name: "TSMC", highestPrice: 478.00, lowestPrice: 472.00, endPrice: 472.50, month: 9, day: 7), StockPrice(name: "TSMC", highestPrice: 475.00, lowestPrice: 472.00, endPrice: 475.00, month: 9, day: 8) ] } ``` ## View 這邊要記得 ```import Charts```,因為我們要顯示 AreaMark 在畫面上 然後這邊宣告了一個 ViewModel 的變數 vm 並在前面加上 ```@State``` 修飾字,讓 SwiftUI 來幫我們管理 ViewModel 狀態 接著是 Charts 的語法,語法也是很簡單,像是下面這樣 ```swift // 1:vm.stockData,圖表的資料來源 Chart(vm.stockData) { AreaMark( x: .value("Date", $0.date), // 2:x 軸要顯示的資料 y: .value("End Price", $0.endPrice) // 3:y 軸要顯示的資料 ) .foregroundStyle(by: .value("Stock Name", $0.name)) // 4:左下角的圖例樣式 or 圖表的外觀樣式 } ``` 或者你也可以透過 ForEach 來寫,只是就會要讓 Model 繼承 ```Identifiable``` 並宣告 UUID() 變數在 Model 裡面,像是這樣 ```var id = UUID().uuidString``` ```swift Chart { // 1:vm.stockData,圖表的資料來源 ForEach(vm.stockData) { data in AreaMark( x: .value("Date", data.date), // 2:x 軸要顯示的資料 y: .value("End Price", data.endPrice) // 3:y 軸要顯示的資料 ) .foregroundStyle(by: .value("Stock Name", data.name)) // 4:左下角的圖例樣式 or 圖表的外觀樣式 } } ``` 完整程式碼如下: ```swift import SwiftUI import Charts struct AreaMarkView: View { @State private var vm = StockPriceViewModel() var body: some View { Chart { ForEach(vm.stockData) { data in AreaMark( x: .value("Date", data.date), y: .value("End Price", data.endPrice) ) .foregroundStyle(by: .value("Stock Name", data.name)) } } .frame(height: 300) .padding() } } ``` ![](https://i.imgur.com/WJp5vpK.png) ## 來點變化吧 讓我們再加一間公司的股價上去好了,就是你了,友達 一樣也是使用 8/19~9/8 這段期間的收盤價,來當作我們的圖表資料 所以現在我們的 ViewModel 就會像是下面這樣 ```swift class StockPriceViewModel { var stockData: [StockPrice] = [ // MARK: TSMC Stock Price StockPrice(name: "TSMC", price: 519.00, month: 8, day: 19), StockPrice(name: "TSMC", price: 510.00, month: 8, day: 22), StockPrice(name: "TSMC", price: 504.00, month: 8, day: 23), StockPrice(name: "TSMC", price: 503.00, month: 8, day: 24), StockPrice(name: "TSMC", price: 508.00, month: 8, day: 25), StockPrice(name: "TSMC", price: 512.00, month: 8, day: 26), StockPrice(name: "TSMC", price: 498.50, month: 8, day: 29), StockPrice(name: "TSMC", price: 496.00, month: 8, day: 30), StockPrice(name: "TSMC", price: 505.00, month: 8, day: 31), StockPrice(name: "TSMC", price: 490.50, month: 9, day: 1), StockPrice(name: "TSMC", price: 485.00, month: 9, day: 2), StockPrice(name: "TSMC", price: 486.00, month: 9, day: 5), StockPrice(name: "TSMC", price: 489.00, month: 9, day: 6), StockPrice(name: "TSMC", price: 472.50, month: 9, day: 7), StockPrice(name: "TSMC", price: 475.00, month: 9, day: 8), // MARK: AUO Stock Price StockPrice(name: "AUO", price: 16.95, month: 8, day: 19), StockPrice(name: "AUO", price: 16.95, month: 8, day: 22), StockPrice(name: "AUO", price: 16.00, month: 8, day: 23), StockPrice(name: "AUO", price: 16.15, month: 8, day: 24), StockPrice(name: "AUO", price: 16.10, month: 8, day: 25), StockPrice(name: "AUO", price: 16.15, month: 8, day: 26), StockPrice(name: "AUO", price: 15.75, month: 8, day: 29), StockPrice(name: "AUO", price: 16.40, month: 8, day: 30), StockPrice(name: "AUO", price: 16.75, month: 8, day: 31), StockPrice(name: "AUO", price: 17.10, month: 9, day: 1), StockPrice(name: "AUO", price: 16.90, month: 9, day: 2), StockPrice(name: "AUO", price: 17.40, month: 9, day: 5), StockPrice(name: "AUO", price: 17.50, month: 9, day: 6), StockPrice(name: "AUO", price: 17.40, month: 9, day: 7), StockPrice(name: "AUO", price: 17.35, month: 9, day: 8), ] } ``` 現在的圖表會長得像下面這樣 ![](https://i.imgur.com/0R0lU7R.png) 但這樣友達的資料好像哪裡怪怪的,跑到上面了 這是因為 AreaMark 的 init 預設 stacking 是設為 .standard ```swift public init<X, Y>(x: PlottableValue<X>, y: PlottableValue<Y>, stacking: MarkStackingMethod = .standard) where X : Plottable, Y : Plottable ``` 而 stacking 有哪幾種樣式可以選呢~一共有四種~ ```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 } } ``` 下面就來展現四種不同 stacking 的圖表長什麼樣 ![](https://i.imgur.com/CXwY3uz.png =385x625) ▲ stacking: .standard ![](https://i.imgur.com/ThQ86Zz.png =380x625) ▲ stacking: .normalized ![](https://i.imgur.com/enPNBvf.png =380x630) ▲ stacking: .center ![](https://i.imgur.com/7Hb7hxm.png =375x625) ▲ stacking: .unstacked AreaMark 還可以繪製有 Range 的圖表,讓我們一起看一下該如何實作 首先,我們先幫 Model 新增兩個欄位,來代表當日股價的高點跟低點 ```swift struct StockPrice: Identifiable { var id = UUID().uuidString let name: String // 公司名稱 let highestPrice: Double // 當日最高點 let lowestPrice: Double // 當日最低點 let endPrice: Double // 當日收盤價 let date: Date // 日期 init(name: String, highestPrice: Double, lowestPrice: Double, endPrice: Double, month: Int, day: Int) { self.name = name self.highestPrice = highestPrice self.lowestPrice = lowestPrice self.endPrice = endPrice let calender = Calendar.autoupdatingCurrent self.date = calender.date(from: DateComponents(month: month, day: day))! } } ``` 並將 ViewModel 更新一下,這裡就以台積電為例就好 ```swift class StockPriceViewModel { var stockData: [StockPrice] = [ // MARK: TSMC Stock Price StockPrice(name: "TSMC", highestPrice: 523.00, lowestPrice: 517.00, endPrice: 519.00, month: 8, day: 19), StockPrice(name: "TSMC", highestPrice: 514.00, lowestPrice: 510.00, endPrice: 510.00, month: 8, day: 22), StockPrice(name: "TSMC", highestPrice: 506.00, lowestPrice: 502.00, endPrice: 504.00, month: 8, day: 23), StockPrice(name: "TSMC", highestPrice: 508.00, lowestPrice: 503.00, endPrice: 503.00, month: 8, day: 24), StockPrice(name: "TSMC", highestPrice: 510.00, lowestPrice: 504.00, endPrice: 508.00, month: 8, day: 25), StockPrice(name: "TSMC", highestPrice: 515.00, lowestPrice: 511.00, endPrice: 512.00, month: 8, day: 26), StockPrice(name: "TSMC", highestPrice: 502.00, lowestPrice: 496.00, endPrice: 498.50, month: 8, day: 29), StockPrice(name: "TSMC", highestPrice: 500.00, lowestPrice: 496.00, endPrice: 496.00, month: 8, day: 30), StockPrice(name: "TSMC", highestPrice: 505.00, lowestPrice: 492.00, endPrice: 505.00, month: 8, day: 31), StockPrice(name: "TSMC", highestPrice: 495.50, lowestPrice: 490.00, endPrice: 490.50, month: 9, day: 1), StockPrice(name: "TSMC", highestPrice: 489.50, lowestPrice: 485.00, endPrice: 485.00, month: 9, day: 2), StockPrice(name: "TSMC", highestPrice: 488.00, lowestPrice: 484.00, endPrice: 486.00, month: 9, day: 5), StockPrice(name: "TSMC", highestPrice: 491.50, lowestPrice: 486.50, endPrice: 489.00, month: 9, day: 6), StockPrice(name: "TSMC", highestPrice: 478.00, lowestPrice: 472.00, endPrice: 472.50, month: 9, day: 7), StockPrice(name: "TSMC", highestPrice: 475.00, lowestPrice: 472.00, endPrice: 475.00, month: 9, day: 8), ] } ``` 而 View 也更新一下,像是下面這樣 ```swift import SwiftUI import Charts struct AreaMarkView: View { @State private var vm = StockPriceViewModel() var body: some View { Chart { ForEach(vm.stockData) { data in AreaMark( x: .value("Date", data.date), yStart: .value("Lowest Price", data.lowestPrice), // y 軸的起點 yEnd: .value("Highest Price", data.highestPrice) // y 軸的終點 ) .foregroundStyle(by: .value("Stock Name", data.name)) } } .frame(height: 300) .padding() } } ``` ![](https://i.imgur.com/hphGi1x.png =555x315) 如果想要為 Charts 新增 X-Axis 或是 Y-Axis 說明的話,可以這樣寫 ```swift Chart { ForEach(vm.stockData) { data in AreaMark( x: .value("Date", data.date), y: .value("End Price", data.endPrice), stacking: .unstacked ) .foregroundStyle(by: .value("Stock Name", data.name)) } } .chartXAxisLabel("Date (2022/8/19~2022/9/8)", alignment: .leading) .chartYAxisLabel("Price (NTD)", alignment: .trailing) ``` ![](https://i.imgur.com/OF7wyuN.png =380x630) ## 總結 這篇簡單實作了 Swift Charts 中的 AreaMark 明天會來介紹 Swift Charts 中的 LineMark,讓我們繼續看下去吧~ ## 參考資料 > 1. [https://developer.apple.com/documentation/charts/areamark](https://developer.apple.com/documentation/charts/areamark)