###### 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)