iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0
Mobile Development

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

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

  • 分享至 

  • xImage
  •  

上一篇介紹了 LineMark 的實作,今天要來介紹的是 Swift Charts 的 PointMark

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

public init<X, Y>(x: PlottableValue<X>, 
                  y: PlottableValue<Y>) where X : Plottable, Y : Plottable

public init<X>(x: PlottableValue<X>, y: CGFloat? = nil) where X : Plottable

public init<Y>(x: CGFloat? = nil, y: PlottableValue<Y>) where Y : 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

class StockPriceViewModel {

    var stockData: [StockPrice] = [
        
        // MARK: TSMC Stock Price
        .init(name: "TSMC", highestPrice: 523.00, lowestPrice: 517.00, endPrice: 519.00, month: 8, day: 19),
        .init(name: "TSMC", highestPrice: 514.00, lowestPrice: 510.00, endPrice: 510.00, month: 8, day: 22),
        .init(name: "TSMC", highestPrice: 506.00, lowestPrice: 502.00, endPrice: 504.00, month: 8, day: 23),
        .init(name: "TSMC", highestPrice: 508.00, lowestPrice: 503.00, endPrice: 503.00, month: 8, day: 24),
        .init(name: "TSMC", highestPrice: 510.00, lowestPrice: 504.00, endPrice: 508.00, month: 8, day: 25),
        .init(name: "TSMC", highestPrice: 515.00, lowestPrice: 511.00, endPrice: 512.00, month: 8, day: 26),
        .init(name: "TSMC", highestPrice: 502.00, lowestPrice: 496.00, endPrice: 498.50, month: 8, day: 29),
        .init(name: "TSMC", highestPrice: 500.00, lowestPrice: 496.00, endPrice: 496.00, month: 8, day: 30),
        .init(name: "TSMC", highestPrice: 505.00, lowestPrice: 492.00, endPrice: 505.00, month: 8, day: 31),
        .init(name: "TSMC", highestPrice: 495.50, lowestPrice: 490.00, endPrice: 490.50, month: 9, day: 1),
        .init(name: "TSMC", highestPrice: 489.50, lowestPrice: 485.00, endPrice: 485.00, month: 9, day: 2),
        .init(name: "TSMC", highestPrice: 488.00, lowestPrice: 484.00, endPrice: 486.00, month: 9, day: 5),
        .init(name: "TSMC", highestPrice: 491.50, lowestPrice: 486.50, endPrice: 489.00, month: 9, day: 6),
        .init(name: "TSMC", highestPrice: 478.00, lowestPrice: 472.00, endPrice: 472.50, month: 9, day: 7),
        .init(name: "TSMC", highestPrice: 475.00, lowestPrice: 472.00, endPrice: 475.00, month: 9, day: 8),
        .init(name: "TSMC", highestPrice: 491.00, lowestPrice: 485.00, endPrice: 486.00, month: 9, day: 12),
        .init(name: "TSMC", highestPrice: 495.00, lowestPrice: 491.00, endPrice: 493.00, month: 9, day: 13),
        .init(name: "TSMC", highestPrice: 482.50, lowestPrice: 476.00, endPrice: 480.00, month: 9, day: 14),
        .init(name: "TSMC", highestPrice: 480.00, lowestPrice: 476.00, endPrice: 476.50, month: 9, day: 15),
        .init(name: "TSMC", highestPrice: 472.00, lowestPrice: 469.00, endPrice: 472.00, month: 9, day: 16),
        .init(name: "TSMC", highestPrice: 473.00, lowestPrice: 466.00, endPrice: 467.00, month: 9, day: 19),
        .init(name: "TSMC", highestPrice: 478.00, lowestPrice: 470.00, endPrice: 476.50, month: 9, day: 20),
        .init(name: "TSMC", highestPrice: 475.50, lowestPrice: 468.50, endPrice: 471.00, month: 9, day: 21),
        .init(name: "TSMC", highestPrice: 468.00, lowestPrice: 459.00, endPrice: 464.50, month: 9, day: 22),
        .init(name: "TSMC", highestPrice: 460.50, lowestPrice: 455.00, endPrice: 455.00, month: 9, day: 23),
        .init(name: "TSMC", highestPrice: 454.00, lowestPrice: 443.00, endPrice: 446.50, month: 9, day: 26),
        .init(name: "TSMC", highestPrice: 451.50, lowestPrice: 446.00, endPrice: 448.00, month: 9, day: 27),
        .init(name: "TSMC", highestPrice: 449.00, lowestPrice: 438.00, endPrice: 438.00, month: 9, day: 28),
        .init(name: "TSMC", highestPrice: 443.50, lowestPrice: 432.00, endPrice: 435.00, month: 9, day: 29),
        .init(name: "TSMC", highestPrice: 427.50, lowestPrice: 422.00, endPrice: 422.00, month: 9, day: 30),
        
        // MARK: AUO Stock Price
        .init(name: "AUO", highestPrice: 17.15, lowestPrice: 15.70, endPrice: 16.95, month: 8, day: 19),
        .init(name: "AUO", highestPrice: 16.95, lowestPrice: 16.60, endPrice: 16.95, month: 8, day: 22),
        .init(name: "AUO", highestPrice: 16.80, lowestPrice: 15.75, endPrice: 16.00, month: 8, day: 23),
        .init(name: "AUO", highestPrice: 16.40, lowestPrice: 16.05, endPrice: 16.15, month: 8, day: 24),
        .init(name: "AUO", highestPrice: 16.30, lowestPrice: 15.95, endPrice: 16.10, month: 8, day: 25),
        .init(name: "AUO", highestPrice: 16.25, lowestPrice: 16.00, endPrice: 16.15, month: 8, day: 26),
        .init(name: "AUO", highestPrice: 16.05, lowestPrice: 15.40, endPrice: 15.75, month: 8, day: 29),
        .init(name: "AUO", highestPrice: 16.70, lowestPrice: 16.15, endPrice: 16.40, month: 8, day: 30),
        .init(name: "AUO", highestPrice: 16.75, lowestPrice: 16.15, endPrice: 16.75, month: 8, day: 31),
        .init(name: "AUO", highestPrice: 17.25, lowestPrice: 16.45, endPrice: 17.10, month: 9, day: 1),
        .init(name: "AUO", highestPrice: 17.35, lowestPrice: 16.70, endPrice: 16.90, month: 9, day: 2),
        .init(name: "AUO", highestPrice: 17.45, lowestPrice: 16.80, endPrice: 17.40, month: 9, day: 5),
        .init(name: "AUO", highestPrice: 17.70, lowestPrice: 17.25, endPrice: 17.50, month: 9, day: 6),
        .init(name: "AUO", highestPrice: 17.60, lowestPrice: 17.10, endPrice: 17.40, month: 9, day: 7),
        .init(name: "AUO", highestPrice: 17.60, lowestPrice: 17.30, endPrice: 17.35, month: 9, day: 8),
        .init(name: "AUO", highestPrice: 17.60, lowestPrice: 17.25, endPrice: 17.25, month: 9, day: 12),
        .init(name: "AUO", highestPrice: 17.50, lowestPrice: 17.20, endPrice: 17.35, month: 9, day: 13),
        .init(name: "AUO", highestPrice: 17.20, lowestPrice: 16.95, endPrice: 17.05, month: 9, day: 14),
        .init(name: "AUO", highestPrice: 18.00, lowestPrice: 17.25, endPrice: 18.00, month: 9, day: 15),
        .init(name: "AUO", highestPrice: 18.15, lowestPrice: 17.80, endPrice: 18.10, month: 9, day: 16),
        .init(name: "AUO", highestPrice: 18.10, lowestPrice: 17.65, endPrice: 17.95, month: 9, day: 19),
        .init(name: "AUO", highestPrice: 18.05, lowestPrice: 17.10, endPrice: 17.10, month: 9, day: 20),
        .init(name: "AUO", highestPrice: 17.45, lowestPrice: 16.30, endPrice: 16.80, month: 9, day: 21),
        .init(name: "AUO", highestPrice: 16.70, lowestPrice: 16.05, endPrice: 16.25, month: 9, day: 22),
        .init(name: "AUO", highestPrice: 16.35, lowestPrice: 15.80, endPrice: 15.80, month: 9, day: 23),
        .init(name: "AUO", highestPrice: 15.60, lowestPrice: 15.05, endPrice: 15.10, month: 9, day: 26),
        .init(name: "AUO", highestPrice: 15.40, lowestPrice: 15.00, endPrice: 15.05, month: 9, day: 27),
        .init(name: "AUO", highestPrice: 15.00, lowestPrice: 14.60, endPrice: 14.70, month: 9, day: 28),
    ]
}

View

這邊要記得 import Charts,因為我們要顯示 PointMark 在畫面上

然後這邊宣告了一個 ViewModel 的變數 vm
並在前面加上 @State 修飾字,讓 SwiftUI 來幫我們管理 ViewModel 狀態

接著是 Charts 的語法,語法也是很簡單,像是下面這樣

// 1:vm.stockData,圖表的資料來源
Chart(vm.stockData) {
    PointMark(
        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
        PointMark(
            x: .value("Date", data.date), // 2:x 軸要顯示的資料
            y: .value("End Price", data.endPrice) // 3:y 軸要顯示的資料
        )
        .foregroundStyle(by: .value("Stock Name", data.name)) // 4:左下角的圖例樣式 or 圖表的外觀樣式
    }
}

讓圖案樣式可以自由選擇

現在的圖應該會長得像是下面這樣

那除了圓形樣式的 symbol,還可以更換成其他的圖案嗎?答案是可以的!

加圖案的語法也很簡單,像是下面這樣

Chart {
    ForEach(vm.stockData) { data in
        PointMark(
            x: .value("Date", data.date),
            y: .value("End Price", data.endPrice)
        )
        .foregroundStyle(by: .value("Stock Name", data.name))
        .symbol(.square) // 圖案類型
        .symbolSize(100) // 圖案大小
    }
}

加完以後,現在會長的像是這樣

那總共有哪些圖案呢?讓我們來一起看一下~

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
extension ChartSymbolShape where Self == BasicChartSymbolShape {

    /// 圓形樣式
    public static var circle: BasicChartSymbolShape { get }

    /// 矩形樣式
    public static var square: BasicChartSymbolShape { get }

    /// 三角形樣式
    public static var triangle: BasicChartSymbolShape { get }

    /// 菱形樣式
    public static var diamond: BasicChartSymbolShape { get }

    /// 五邊形樣式
    public static var pentagon: BasicChartSymbolShape { get }

    /// 加號樣式
    public static var plus: BasicChartSymbolShape { get }

    /// 十字樣式
    public static var cross: BasicChartSymbolShape { get }

    /// 雪花樣式
    public static var asterisk: BasicChartSymbolShape { get }
}

基本版

首先我們先宣告一個 symbol 變數,以及建立一個 Menu

@State private var symbol: BasicChartSymbolShape = .square
Menu {
    
} label: {
    
}

然後在 label: 這個 Closure 裡面顯示一個 Text,用來當作提示使用者的文字,像是下面這樣

Menu {
    
} label: {
    Text("Choose Symbol")   
}

接著,我們要來製作 Menu 裡面要顯示的選項,製作之前,先來看一下 Apple 是怎麼定義 Menu 的

在 content 這個 Closure 裡面要放的是 Menu 的選項,而選項都是透過 Button 來宣告的

所以如果要建立所有圖案樣式的 Menu 的話,可以像下面這樣寫

Menu {
    Button {
        symbol = .square
    } label: {
        Label("Square", systemImage: "square")
    }
    Button {
        symbol = .circle
    } label: {
        Label("Circle", systemImage: "circle")
    }
    Button {
        symbol = .triangle
    } label: {
        Label("Triangle", systemImage: "triangle")
    }
    Button {
        symbol = .diamond
    } label: {
        Label("Diamond", systemImage: "diamond")
    }
    Button {
        symbol = .pentagon
    } label: {
        Label("Pentagon", systemImage: "pentagon")
    }
    Button {
        symbol = .plus
    } label: {
        Label("Plus", systemImage: "plus")
    }
    Button {
        symbol = .cross
    } label: {
        Label("Cross", systemImage: "cross")
    }
    Button {
        symbol = .asterisk
    } label: {
        Label("Asterisk", systemImage: "asterisk")
    }
} label: {
    Text("Choose Symbol")
}

現在畫面上看起來會長的像這樣

優化版

但這樣看起來,Code 實在太醜了,讓我們來做點修改吧

首先先建立一個叫做 MenuSymbolButton 的 SwiftUI View,像是下面這樣

struct MenuSymbolButton: View {
    
    var body: some View {

    }
}

然後在 body 裡面,宣告一個 Button 元件,像是下面這樣

struct MenuSymbolButton: View {
    
    var body: some View {
        Button {

        } label: {
            
        }
    }
}

接著在宣告要用來顯示每個圖案選項的 圖案文字 跟 圖案的圖片名稱 的變數

var symbolName: String // 圖案文字

var symbolImageName: String // 圖案的圖片名稱

接下來是最重要的一步,將連結兩個 View 之間的橋樑就在這裡了

宣告一個 @Binding 的 symbol 變數,用來跟另外一個 View 裡面的 @State symbol 變數進行綁定

@Binding var symbol: BasicChartSymbolShape

到這邊 MenuSymbolButton 所需的變數都宣告好了,就讓我們組合起來吧
現在的 MenuSymbolButton 應該會長得像下面這樣

struct MenuSymbolButton: View {
   
    @Binding var symbol: BasicChartSymbolShape
    
    var symbolName: String
    
    var symbolImageName: String
    
    var body: some View {
        Button {

        } label: {

        }
    }
}

我們先來組合 label: 裡面要顯示的內容

Button {

} label: {
    Label(symbolName, systemImage: symbolImageName)
}

接著再組合 action: 裡面要做的事情

這裡是透過 symbolImageName 來進行 switch

Button {
    symbol = {
        switch symbolImageName {
        case "square": return .square
        case "circle": return .circle
        case "triangle": return .triangle
        case "diamond": return .diamond
        case "pentagon": return .pentagon
        case "plus": return .plus
        case "cross": return .cross
        case "asterisk": return .asterisk
        default: return .square
        }
    }()
} label: {
    Label(symbolName, systemImage: symbolImageName)
}

將原本的 Menu 改寫

剛才將 MenuSymbolButton 設計好了,就要來將原本的一長串 Button 進行改寫了~

改寫完,就長的像是下面這樣

Menu {
    MenuSymbolButton(symbol: $symbol, symbolName: "Square", symbolImageName: "square")
    MenuSymbolButton(symbol: $symbol, symbolName: "Circle", symbolImageName: "circle")
    MenuSymbolButton(symbol: $symbol, symbolName: "Triangle", symbolImageName: "triangle")
    MenuSymbolButton(symbol: $symbol, symbolName: "Diamond", symbolImageName: "diamond")
    MenuSymbolButton(symbol: $symbol, symbolName: "Pentagon", symbolImageName: "pentagon")
    MenuSymbolButton(symbol: $symbol, symbolName: "Plus", symbolImageName: "plus")
    MenuSymbolButton(symbol: $symbol, symbolName: "Cross", symbolImageName: "cross")
    MenuSymbolButton(symbol: $symbol, symbolName: "Asterisk", symbolImageName: "asterisk")
} label: {
    Text("Choose Symbol")
}

將 Charts 的 symbol 修飾字改寫

到這邊,就只剩下最後一步要做了

現在 Charts 的 symbol 還是透過直接給值的方式,來做設定

現在就要來將這邊做改寫!改寫完會長的像下面這樣

Chart {
    ForEach(vm.stockData) { data in
        LineMark(
            x: .value("Date", data.date),
            y: .value("End Price", data.endPrice)
        )
        .foregroundStyle(by: .value("Stock Name", data.name))
        .symbol(symbol) // 原本是直接給 .square,現在改成給 symbol 變數,讓 SwiftUI 自動更新變數狀態
        .symbolSize(100)
    }
}

完整程式碼

import SwiftUI
import Charts

struct PointMarkView: View {
    
    @State private var symbol: BasicChartSymbolShape = .square
    
    @State private var vm = StockPriceViewModel()
    
    var body: some View {
        VStack {
            Chart {
                ForEach(vm.stockData) { stock in
                    PointMark(
                        x: .value("Date", stock.date),
                        y: .value("End Price", stock.endPrice)
                    )
                    .foregroundStyle(by: .value("Stock Name", stock.name))
                    .symbol(symbol)
                    .symbolSize(100)
                }
            }
            .chartXAxisLabel("Date (2022/8/19~2022/9/30)", alignment: .leading)
            .chartYAxisLabel("Price (NTD)", alignment: .trailing)
            .frame(height: 300)
            .padding()
            
            Menu {
                MenuSymbolButton(symbol: $symbol, symbolName: "Square", symbolImageName: "square")
                MenuSymbolButton(symbol: $symbol, symbolName: "Circle", symbolImageName: "circle")
                MenuSymbolButton(symbol: $symbol, symbolName: "Triangle", symbolImageName: "triangle")
                MenuSymbolButton(symbol: $symbol, symbolName: "Diamond", symbolImageName: "diamond")
                MenuSymbolButton(symbol: $symbol, symbolName: "Pentagon", symbolImageName: "pentagon")
                MenuSymbolButton(symbol: $symbol, symbolName: "Plus", symbolImageName: "plus")
                MenuSymbolButton(symbol: $symbol, symbolName: "Cross", symbolImageName: "cross")
                MenuSymbolButton(symbol: $symbol, symbolName: "Asterisk", symbolImageName: "asterisk")
            } label: {
                Text("Choose Symbol")
            }
        }
    }
}

struct PointMarkView_Previews: PreviewProvider {
    static var previews: some View {
        PointMarkView()
    }
}

struct MenuSymbolButton: View {
   
    @Binding var symbol: BasicChartSymbolShape
    
    var symbolName: String
    
    var symbolImageName: String
    
    var body: some View {
        Button {
            symbol = {
                switch symbolImageName {
                case "square": return .square
                case "circle": return .circle
                case "triangle": return .triangle
                case "diamond": return .diamond
                case "pentagon": return .pentagon
                case "plus": return .plus
                case "cross": return .cross
                case "asterisk": return .asterisk
                default: return .square
                }
            }()
        } label: {
            Label(symbolName, systemImage: symbolImageName)
        }
    }
}

總結

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

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

參考資料

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

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

尚未有邦友留言

立即登入留言