iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0
Mobile Development

在 iOS 開發路上的大小事2系列 第 24

【在 iOS 開發路上的大小事2-Day24】來自 Apple 爸爸的最新力作 - Swift Charts 之 AreaMark 實作篇

  • 分享至 

  • xImage
  •  

在上一篇簡單介紹了 Swift Charts ,從這篇開始,透過 6 天實作,讓你快速上手

今天要來實作的是 Swift Charts 的 AreaMark

AreaMark 一共提供了六種 init 的方法,讓開發者可以繪製不同樣式的圖表

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

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 這段期間的收盤價,來當作我們的圖表資料

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 的語法,語法也是很簡單,像是下面這樣

// 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

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 圖表的外觀樣式
    }
}

完整程式碼如下:

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()
    }
}

來點變化吧

讓我們再加一間公司的股價上去好了,就是你了,友達

一樣也是使用 8/19~9/8 這段期間的收盤價,來當作我們的圖表資料

所以現在我們的 ViewModel 就會像是下面這樣

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),
    ]
}

現在的圖表會長得像下面這樣

但這樣友達的資料好像哪裡怪怪的,跑到上面了

這是因為 AreaMark 的 init 預設 stacking 是設為 .standard

public init<X, Y>(x: PlottableValue<X>, 
                  y: PlottableValue<Y>, 
                  stacking: MarkStackingMethod = .standard) where X : Plottable, Y : Plottable

而 stacking 有哪幾種樣式可以選呢~一共有四種~

@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 的圖表長什麼樣


▲ stacking: .standard


▲ stacking: .normalized


▲ stacking: .center


▲ stacking: .unstacked

AreaMark 還可以繪製有 Range 的圖表,讓我們一起看一下該如何實作

首先,我們先幫 Model 新增兩個欄位,來代表當日股價的高點跟低點

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 更新一下,這裡就以台積電為例就好

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 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()
    }
}

如果想要為 Charts 新增 X-Axis 或是 Y-Axis 說明的話,可以這樣寫

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)

總結

這篇簡單實作了 Swift Charts 中的 AreaMark

明天會來介紹 Swift Charts 中的 LineMark,讓我們繼續看下去吧~

參考資料

  1. https://developer.apple.com/documentation/charts/areamark

上一篇
【在 iOS 開發路上的大小事2-Day23】來自 Apple 爸爸的最新力作 - Swift Charts 之介紹篇
下一篇
【在 iOS 開發路上的大小事2-Day25】來自 Apple 爸爸的最新力作 - Swift Charts 之 LineMark 實作篇
系列文
在 iOS 開發路上的大小事230
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言