D14 - 用 Swift 和公开资讯,打造投资理财的 Apps { 加权指数K线图实作.2 }

制作 K 线的 Data Model,从前面文章 [加权指数K线图实作.2] 的 response 可以知道,我们的 csv 档需要 [开]、[高]、[低]、[收] 四个值。考虑到台股所有股票都会用到技术分析,再加上 stockCode 和 stockName,让这个 Data Model 可以用在其他股票项目上。

import Foundation

struct StockKLine {
    
    let stockCode: String
    let stockName: String
    
    let dateString: String

    let openString: String
    let highestString: String
    let lowestString: String
    let closeString: String
}

在完成 Data Model 的宣告之後,就回去修改 TwStockKLineManager。之前传出来的是 String,但现在已经有 StockKLine 的 Data Model 了,那就应该传出对应的型别,而不是 String了。

TwStockKLineManager parse csv,并传出 KLine model 的程序码如下

import Foundation

//加权指数-公开资讯观测站 https://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.html
// https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907
class TwStockKLineManager {
    
    private var dateUtility: DateUtility {
        return DateUtility()
    }
    
    private lazy var alamofireAdapter: AlamofireAdapter = {
        return AlamofireAdapter()
    }()
    
    func requestTwStockKLine(date: Date, completion: @escaping (([StockKLine], Error?) -> Void)) {
        
        let format = "yyyyMMdd"
        let string = dateUtility.getString(date: date, format: format)
        
        let urlString = "https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=\(string)"
        
        alamofireAdapter.requestForString(urlString, method: .get) { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .success(let string):
                let kLineDataSet = self.convertToKLineList(string)
                completion(kLineDataSet, nil)
            case .failure(let error):
                completion([], error)
            }
        }
    }
    
    private func convertToKLineList(_ string: String) -> [StockKLine] {
        
        var kLineDataSet = [StockKLine]()
        
        let trimmedString = trimmedFirstLine(string)
        
        if let csv = try? CSVAdapter(rawString: trimmedString) {
            
            for each in csv.namedRows {
                
                let stockCode = "taiex"
                let stockName = "台股加权指数"
                
                let dateString = each["Date"] ?? ""
                let openString = each["Opening Index"] ?? ""
                let highestString = each["Highest Index"] ?? ""
                let lowestString = each["Lowest Index"] ?? ""
                let closeString = each["Closing Index"] ?? ""
                
                let kLine = StockKLine(stockCode: stockCode, stockName: stockName, dateString: dateString, openString: openString, highestString: highestString, lowestString: lowestString, closeString: closeString)
                
                kLineDataSet.append(kLine)
            }
        }
        
        return kLineDataSet
    }
    
    private func trimmedFirstLine(_ string: String) -> String {
        
        return CSVAdapter.removeLine(string, at: 1)
    }
}

接下来,在 TwStockMarketKLineModel 中,加上让 VC 呼叫,可取得 K 线资料。在呼叫後,只有 Model 知道要拿取几个月份的资料,因前述设定,为取得当月和上一个月的 K 线资料,所以 requestTwExKLineInfo() 会呼叫 requestTwExThisMonthKLineInfo() 和 requestTwExLastMonthKLineInfo()。

/// 会取这个月和前一个月台股加权指的 KLine data,单一个月,有可能 k 棒数量太少
    func requestTwExKLineInfo() {
        
        requestTwExThisMonthKLineInfo()
        requestTwExLastMonthKLineInfo()
    }
    
    private func requestTwExThisMonthKLineInfo() {
        
        let date = dateUtility.getStartOfMonth()
        manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
            
            self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
            self?.update(kLineDataSet)
        }
    }
    
    private func requestTwExLastMonthKLineInfo() {
        
        let date = dateUtility.getLastMonthStartDate()
        manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
            
            self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
            self?.update(kLineDataSet)
        }
    }
    
    private func update(_ dataSet: [StockKLine]) {
        
        let updatedData = Set(self.twExStockDataSet + dataSet)
        self.twExStockDataSet = Array(updatedData).sorted { $0.dateString < $1.dateString }
    }

在完成後,使用 delegate pattern 通知 VC 有资料更新。

整个 TwStockMarketKLineModelDelegate 的程序码如下

import Foundation

protocol TwStockMarketKLineModelDelegate: AnyObject {
    
    func didRecieveTaiEx(kLineDataSet: [StockKLine], error: Error?)
}

class TwStockMarketKLineModel {
    
    weak var delegate: TwStockMarketKLineModelDelegate?
    
    private var dateUtility: DateUtility {
        return DateUtility()
    }
    
    private lazy var manager: TwStockKLineManager = {
        return TwStockKLineManager()
    }()
    
    var twExStockDataSet = [StockKLine]()
    
    /// 会取这个月和前一个月台股加权指的 KLine data,单一个月,有可能 k 棒数量太少
    func requestTwExKLineInfo() {
        
        requestTwExThisMonthKLineInfo()
        requestTwExLastMonthKLineInfo()
    }
    
    private func requestTwExThisMonthKLineInfo() {
        
        let date = dateUtility.getStartOfMonth()
        manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
            
            self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
            self?.update(kLineDataSet)
        }
    }
    
    private func requestTwExLastMonthKLineInfo() {
        
        let date = dateUtility.getLastMonthStartDate()
        manager.requestTwStockKLine(date: date) { [weak self] kLineDataSet, error in
            
            self?.delegate?.didRecieveTaiEx(kLineDataSet: kLineDataSet, error: error)
            self?.update(kLineDataSet)
        }
    }
    
    private func update(_ dataSet: [StockKLine]) {
        
        let updatedData = Set(self.twExStockDataSet + dataSet)
        self.twExStockDataSet = Array(updatedData).sorted { $0.dateString < $1.dateString }
    }
}

然後在 VC 中设定好成为发动 Model 的 Button action,并成为 Model 的 delegate 即可。

import UIKit

class TwStockMarketKLineViewController: UIViewController {
    
    @IBOutlet weak var debugTextView: UITextView!
    
    private lazy var model: TwStockMarketKLineModel = {
        let model = TwStockMarketKLineModel()
        model.delegate = self
        return model
    }()

    // MARK: - life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // MARK: - IBAction
    @IBAction func fetchTwStockKLineButtonDidTap(_ sender: Any) {
        model.requestTwExKLineInfo()
    }
    
    @IBAction func dubugButtonDidTap(_ sender: Any) {
        
        var string = ""
        
        for kLine in model.twExStockDataSet {
            
            string += "\(kLine)\n"
        }
        
        debugTextView.text = string
    }
}

extension TwStockMarketKLineViewController: TwStockMarketKLineModelDelegate {
    
    func didRecieveTaiEx(kLineDataSet: [StockKLine], error: Error?) {
        
        print(model.twExStockDataSet)
    }
}

为了 debug 方便,在这边加上 textView 和 debug button

https://ithelp.ithome.com.tw/upload/images/20210923/20140622Lmoe5bpGRh.png

接下来,就可以专心的画图了,因为你已经取得了需要呈现的资料。

下方是这次 D1 ~ D12 的完成品,可以下载来试
App Store - 台股申购日历

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


<<:  第9天~接续第2页-

>>:  Day9 - 2D渲染环境基础篇 V[Canvas动画概论] - 成为Canvas Ninja ~ 理解2D渲染的精髓

二元树左到右查找 - DAY 16

前序检查(preorder) 中序检查(inorder) 後序检查(postorder) 後序检查来...

React中的优先级

点击进入React源码调试仓库。 UI产生交互的根本原因是各种事件,这也就意味着事件与更新有着直接关...

[Day10] 第十章-专案开启前的User Story (软件工程分享)

前言 在前面总结完laravel的技巧後 今天想要跟各位分享 开启一个专案的时候 会进行什麽步骤 U...

Day 1 - 前言,写作动机分享与准备事项

去年参加 Software Development 类别的铁人赛,主题为PHP 大师之路 - 开源的...

Day21-不能说的秘密(三)

前言 为了让使用者的密码更安全,昨天介绍了所谓的「加盐杂凑」,虽然加盐杂凑已经足够应付现今 CPU ...