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


<<:  DAY11 制作样板(Template)

>>:  Day11牛肉大变身-义式红椒茄汁牛肉丸

Day_02 系统安装(一)

玩OpenWrt第一步当然是制作系统,下载系统映像的入口其实很多,但这个路径我觉得最直观与便捷。可以...

Youtube Analytics API 教学 - 多维度分析 A+B

「鲑鱼均,因为一场鲑鱼之乱被主管称为鲑鱼世代,广义来说以年龄和脸蛋分类的话这应该算是一种 KNN 的...

Day 27 - 客制化 ListRowPresenter 来实作 Loop Banner 效果 Part3

今天我们要来完成 Banner 的效果啦!! 修改 CustomListRowPresenter 我...

【D20】修改食谱#1:根据市价,模拟改价

前言 假日没有行情,所以只能平日来做取得行情资料的工作,所以今天的文章是根据期货行情,模拟价格修改的...

Day 7 阿里云架设网站-服务器架设与自动化运维

云服务器(Elastic Compute Service,ECS) ECS是阿里云上提供服务器租用的...