iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Mobile Development

使用 Swift 和公開資訊,打造投資理財的 Apps系列 第 17

D16 - 用 Swift 和公開資訊,打造投資理財的 Apps { 加權指數 K 線圖實作.4 - 在 X 軸標上每一根 K 棒的日期 }

目前我們已經做出台股加權指數的 K 線圖,但目前進度的線圖的 x 軸沒有時間,所以當使用者看到這張圖,無法判斷這張圖每根 K 線的日期,是哪一天。所以我們需要轉換 x 軸的 index 到 人類可閱讀的日期。

https://ithelp.ithome.com.tw/upload/images/20210925/20140622vyH8n6IvoY.png

開始之前

因為一個月的交易量大約是 20 日左右,所以當月加上個月的量,也在 40 根左右。所以再加上 2 個月前的 K 線資料,讓資料的數量比較充足。

先擴充 DateUtility ,補上一個 func,輸入 n,回傳與現在日期差 n 個月的第一天 Date。

func getMonthStartDate(date: Date = Date(), add month: Int) -> Date {
        
        let calendar = isoCalendar
        let startOfMonth = getStartOfMonth(date: date)
        
        return calendar.date(byAdding: DateComponents(month: month), to: startOfMonth) ?? Date()
    }

所以要拿兩個月前的資料,就只是下面這一行

let date = dateUtility.getMonthStartDate(date: Date(), add: -2)

然後在 TwStockMarketKLineModel 發動拿取前三個月

/// 會取這個月和前一個月台股加權指的 KLine data,單一個月,有可能 k 棒數量太少
    func requestTwExKLineInfo() {
        
        requestTwExThisMonthKLineInfo() //實作在前面的文章已有
        requestTwExLastMonthKLineInfo() //實作在前面的文章已有
        requestTwExBefore2MonthKLineInfo()
    }

/// 拿前兩個月的 k line
    private func requestTwExBefore2MonthKLineInfo() {
        
        let date = dateUtility.getMonthStartDate(date: Date(), add: -2)
        manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
            
            self?.update(kLineDataSet)
            self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
        }
    }

當畫線的實作已經確認完成了之後,就是整理程式碼。

先開一個 ChartsAdapter,讓這個物件負責整個專案和 Charts 溝通。那首先,把 KLine VC 中和 Charts 相關功能,放進去。

import UIKit
import Charts

class ChartsAdapter {
}

// MARK: - 這一段的程式碼做 K Line charts
extension ChartsAdapter {
    
    /// 讓 VC 在需要 K Line 圖的時候直接拿到一個 K Line View,但為了不讓外部看到 Charts,回傳 UIView
    /// - Returns: 因 CandleStickChartView 繼承 UIView,封裝起來,不讓外部看到 Charts
    func getCandleStickChartView() -> UIView {
        
        let candleView = CandleStickChartView()
        setupCandleStickView(candleView)
        return candleView
    }
    
    func update(stockSticks: [StockKLine], on candleView: UIView) {
        
        let dateUtility = DateUtility()
        
        if let candleView = candleView as? CandleStickChartView {
            let dataEntry = convert(stockStick: stockSticks)
            let dataSet = convert(dataEntry: dataEntry)
            let data = convert(dataSet: dataSet)
            candleView.data = data
            updateMaxMin(candleView, dataSet: dataSet)
        }
    }
    
    private func updateXAxis(_ chartView: CandleStickChartView, indexDateLabels: [Int: String]) {
        
        chartView.xAxis.valueFormatter = CandleXAxisValueFormatter(indexLabelMap: indexDateLabels)
        chartView.xAxis.granularity = 1.0
    }
    
    private func setupCandleStickView(_ chartView: CandleStickChartView) {
        
        chartView.dragEnabled = false
        chartView.setScaleEnabled(true)
        chartView.maxVisibleCount = 200
        chartView.pinchZoomEnabled = true
        
        chartView.legend.horizontalAlignment = .right
        chartView.legend.verticalAlignment = .top
        chartView.legend.orientation = .vertical
        chartView.legend.drawInside = false
        chartView.legend.font = UIFont.systemFont(ofSize: 10)
        
        chartView.leftAxis.labelFont = UIFont.systemFont(ofSize: 10)
        chartView.leftAxis.spaceTop = 0.3
        chartView.leftAxis.spaceBottom = 0.3
        chartView.leftAxis.axisMinimum = 0
        
        chartView.rightAxis.enabled = false
        
        chartView.xAxis.labelPosition = .bottom
        chartView.xAxis.labelFont = UIFont.systemFont(ofSize: 10)
        chartView.xAxis.labelCount = 10
    }
    
    private func convert(stockStick: [StockKLine]) -> [CandleChartDataEntry] {
        
        var dataEntry = [CandleChartDataEntry]()
        
        for (i, each) in stockStick.enumerated() {
            
            let x = Double(i)
            if let open = each.open,
               let highest = each.highest,
               let lowest = each.lowest,
               let close = each.close {
                
                let candleData = CandleChartDataEntry(x: x, shadowH: highest, shadowL: lowest, open: open, close: close)
                dataEntry.append(candleData)
            }
        }
        
        return dataEntry
    }
    
    private func convert(dataEntry: [CandleChartDataEntry]) -> CandleChartDataSet {
        
        let dataSet = CandleChartDataSet(entries: dataEntry)
        
        dataSet.axisDependency = .left
        dataSet.setColor(.red)
        dataSet.drawIconsEnabled = false
        dataSet.shadowColor = .darkGray
        dataSet.shadowWidth = 0.5
        dataSet.decreasingColor = .systemGreen
        dataSet.decreasingFilled = true
        dataSet.increasingColor = .systemRed
        dataSet.increasingFilled = true
        dataSet.neutralColor = .black
        
        dataSet.drawValuesEnabled = false
        
        return dataSet
    }
    
    private func convert(dataSet: CandleChartDataSet) -> CandleChartData {
        
        return CandleChartData(dataSet: dataSet)
    }
    
    private func updateMaxMin(_ chartView: CandleStickChartView, dataSet: CandleChartDataSet) {
        
        let max = dataSet.yMax
        let min = dataSet.yMin
        chartView.leftAxis.axisMaximum = max * 1.05
        chartView.leftAxis.axisMinimum = min * 0.95
    }
}

然後 K Line VC 的程式碼就會少到變成這樣,低於 50 行,而且不會 import Charts,不會和套件耦合。

import UIKit

class KLineViewController: UIViewController {
    
    @IBOutlet weak var chartContainer: UIView!
    
    private lazy var chartsAdapter: ChartsAdapter = {
        return ChartsAdapter()
    }()
    
    private lazy var chartView: UIView = {
        let view = chartsAdapter.getCandleStickChartView()
        return view
    }()
    
    var kLineDataSet = [StockKLine]()

    // MARK: - life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBasicUI()
        setupCandleView()
    }
    
    // MARK: - private methods
    private func setupBasicUI() {
        
        chartContainer.backgroundColor = .clear
        
        chartContainer.addSubview(chartView)
        chartView.translatesAutoresizingMaskIntoConstraints = false
        
        chartView.leadingAnchor.constraint(equalTo: chartContainer.leadingAnchor).isActive = true
        chartView.topAnchor.constraint(equalTo: chartContainer.topAnchor).isActive = true
        chartView.trailingAnchor.constraint(equalTo: chartContainer.trailingAnchor).isActive = true
        chartView.bottomAnchor.constraint(equalTo: chartContainer.bottomAnchor).isActive = true
    }
    
    private func setupCandleView() {
        
        chartsAdapter.update(stockSticks: kLineDataSet, on: chartView)
    }
}

更改 x 軸顯示的資料

在Charts 套件中,可以用 IAxisValueFormatter 這個類別,來告訴 Chart View 在哪個位置要顯示什麼樣的 String。只要該類別 Conform IAxisValueFormatter,並實作 func stringForValue,告訴 Charts,就可以在 x value 顯示你要的值。

在 ChartsAdapter 內宣告 CandleXAxisValueFormatter,要求 init 代入 [Int: String]。

import UIKit
import Charts

extension ChartsAdapter {
    
    class CandleXAxisValueFormatter: IAxisValueFormatter {
        
        private let indexLabelMap: [Int: String]
        
        /// 因為 candle charts 是用 index 來當 x 軸,但是 index 需要 mapping 成 date string,才可以讓人類識別每個 candle stick 代表的意義
        /// - Parameter indexLabelMap: index vs. date string
        init(indexLabelMap: [Int: String]) {
            self.indexLabelMap = indexLabelMap
        }
        
        func stringForValue(_ value: Double, axis: AxisBase?) -> String {
            
            guard let string = indexLabelMap[Int(value)] else {
                return ""
            }
            return string
        }
    }
}

將 Charts 的 x 軸更新的 func 如下

private func updateXAxis(_ chartView: CandleStickChartView, indexDateLabels: [Int: String]) {
        
        chartView.xAxis.valueFormatter = CandleXAxisValueFormatter(indexLabelMap: indexDateLabels)
        chartView.xAxis.granularity = 1.0
    }

在 ChartsAdapter 的對外 func,將 func update(stockSticks: [StockKLine], on candleView: UIView),裡面,在完成 update 後,呼叫更新 XAxis。在 ChartsAdapter 內的 func 更改成下面這樣。

func update(stockSticks: [StockKLine], on candleView: UIView) {
        
        let dateUtility = DateUtility()
        
        var indexDateLabels = [Int: String]()
        
        for (index, stick) in stockSticks.enumerated() {
            
            if let date = stick.date {
                let dateString = dateUtility.getString(date: date, format: "MM/dd")
                indexDateLabels[index] = dateString
            }
        }
        
        if let candleView = candleView as? CandleStickChartView {
            let dataEntry = convert(stockStick: stockSticks)
            let dataSet = convert(dataEntry: dataEntry)
            let data = convert(dataSet: dataSet)
            candleView.data = data
            
            updateXAxis(candleView, indexDateLabels: indexDateLabels)
            updateMaxMin(candleView, dataSet: dataSet)
        }
    }

完成的圖案如下,目標完成,剩下的間距,可以再自行細調。

https://ithelp.ithome.com.tw/upload/images/20210925/20140622zrBgJREvAn.png

下方是這次 D1 ~ D12 的完成品,可以下載來試

App Store - 台股申購日曆

https://ithelp.ithome.com.tw/upload/images/20210924/20140622ypOBM0tgrZ.png


上一篇
D15 - 用 Swift 和公開資訊,打造投資理財的 Apps { 加權指數K線圖實作.3 - 使用 Charts 實作 K 線圖 }
下一篇
D17- 用 Swift 和公開資訊,打造投資理財的 Apps { 移動平均線(MA線)分析 }
系列文
使用 Swift 和公開資訊,打造投資理財的 Apps37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言