D11-用 Swift 和公开资讯,打造投资理财的 Apps { 台股申购实作.4 - 用 Calendar 物件处理台湾的民国年}

股票申购是和时间有关的 feature,所以需要有一个 DateUtility,这个类别负责所有 Date 的处理。

struct DateUtility {
    
    static let dateFormatter = DateFormatter()
}

很多文献都会和你说,Swift 的 DateFormatter() 是个 init 很贵(expensive)的物件,连 Apple 官方文件都建议你写一个 static property 存起来,不要一直 init()。最严重的情况,是有可能卡你 UI 滑动的。

其中有一篇真的实测 init 的秒数的文章,我个人觉得很有深度。连结如下

https://sarunw.com/posts/how-expensive-is-dateformatter/

第一个 func 要写的,就是从 String 转换成 Date 型别。因为不是每个 String 都能转成 Date,所以 return 设计成 optional。

struct DateUtility {
    
    static let dateFormatter = DateFormatter()
    
    func getDate(from string: String, format: String = "yyyy-MM-dd") -> Date? {
        
        DateUtility.dateFormatter.dateFormat = format
        
        return DateUtility.dateFormatter.date(from: string)
    }
}

然後看了一下日期格式…嗯…果然是政府公开资料正常发挥的民国年,不是电脑标准的西元年。不过难度上来说,Big5 处理起来,比较麻烦,民国年转西元年倒是没有那麽困难。

https://ithelp.ithome.com.tw/upload/images/20210920/20140622WRVRU549C7.png

跟据我过去的经验,劝大家千万不要自己手动转换历法

千万不要自己手动转换历法!!

千万不要自己手动转换历法!!!!

不管是哪种语言,尽可能的使用框架中写好的方法进行转换,而且在传值的时候,使用 unix time 来传递,只有在显示前的那一刻,再转换成人类看得懂的格式。

将 dateFormatter 的 calendar property 在读取民国年的时候,用台湾的历法,就可以正确读取以民国年纪录的资料了。

struct DateUtility {
    
    static let dateFormatter = DateFormatter()
    
    private var isoCalendar: Calendar {
        return Calendar(identifier: .iso8601)
    }
    
    private var rocCalendar: Calendar {
        return Calendar(identifier: .republicOfChina)
    }
    
    func getDate(from string: String, format: String = "yyyy-MM-dd") -> Date? {
        
        DateUtility.dateFormatter.calendar = isoCalendar
        DateUtility.dateFormatter.dateFormat = format
        
        return DateUtility.dateFormatter.date(from: string)
    }
    
    func getDateFromTwCalendar(from string: String, format: String = "yyyy/MM/dd") -> Date? {
        
        DateUtility.dateFormatter.calendar = rocCalendar
        DateUtility.dateFormatter.dateFormat = format
        
        return DateUtility.dateFormatter.date(from: string)
    }
}

而 StockSubscription 能接受 yyyy 的输入值,这个功能也归在 DateUtility

func getIntFromDate(component: Calendar.Component) -> Int {
        
        let date = Date()
        let calendar = isoCalendar
        return calendar.component(component, from: date)
    }

然後,在 StockSubscriptionModel 中,加上三种状态,但有可能真的遇到资料有问题,保险起见,我加上第四种 notDefined,如果真的解不出 Date,就让他进入第四种状态。当然,你也可以选择让其中一种状态成为你的预设值啦。但实务上真的,真的,真的不要对後端来的资料用 force unwrap,迟早有一天会出事的。而出事的时候,你就是要修。

extension StockSubscriptionModel {
    
    enum SubscriptionState {
        
        case beforeSubscription
        case duringSubscription
        case finishedSubscription
        case notDefined
    }
}

然後,再用 local time 和 StockSubscriptionInfo 来判断 indexPath 的 info 是哪个状态。当然能取 server time 是最好,但现在状况来说,我并没有後端,所以就用 local time 来当基准。

extension StockSubscriptionModel {
    
    func getSubscriptionState(info: StockSubscriptionInfo) -> SubscriptionState {
        
        let currentTime = Date().timeIntervalSince1970
        
        if let startTime = info.subscriptionStart?.timeIntervalSince1970,
           let endTime = info.subscriptionEnd?.timeIntervalSince1970 {
            
            if currentTime < startTime {
                return .beforeSubscription
            } else if currentTime > endTime {
                return .finishedSubscription
            } else {
                return .duringSubscription
            }
        }
        
        return .notDefined
    }
}

在拿取申购资料的时候,要输入欲取得的年份。但如果写死 2021,那到了 2021-12-31 的时候,你就准备一份改成 2022 的程序码,然後在跨年夜的时候更新。不然使用者的资料就会永远停在 2021 了。

所以我们加上取得客端现在手机的时间,Model 就可以在 12月31日跨到隔年 1月1日的时候,在程序上直接处理了,不用更新程序码。

private func getQueryYear() -> Int {
        
        let dateUtility = DateUtility()
        
        return dateUtility.getIntFromDate(component: .year)
    }

func requestStockSubscription() {
        
        let year = getQueryYear()
        manager.requestStockSubscriptionInfo(year: year) { [weak self] subscriptionList, error in
            
            // 需要去掉中央债的资料
            self?.subscriptionList = self?.filterNotAvailable(subscriptionList) ?? []
            self?.delegate?.didRecieveList(subscriptionList, error: error)
        }
    }

在确定申购状态後, ViewController 在不同的状态下,更新对应的 cell UI风格。

有四种状态,所以就是四种更新 UI 的 func。

private func setBeforeSubscriptionUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "申购未开始"
        cell.stateLabel.textColor = .black
        cell.stateLabel.backgroundColor = .clear
    }
    
    private func setDuringSubscriptionUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "可申购"
        cell.stateLabel.textColor = .systemGreen
        cell.stateLabel.backgroundColor = .clear
    }
    
    private func setFinishedSubscriptionUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "申购结束"
        cell.stateLabel.textColor = .white
        cell.stateLabel.backgroundColor = .systemRed
    }
    
    private func setNotDefinedUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "申购状态未定"
        cell.stateLabel.textColor = .systemGray2
        cell.stateLabel.backgroundColor = .clear
    }

而这个 modify func,是让 VC 在 cellForRow(at:) 呼叫的,在 cellForRow(at:) 会拿到 info 也会拿到 custom tableViewCell。让这个 func 处理每个状态的 UI。

private func modify(_ cell: StockSubscriptionTableViewCell, with info: StockSubscriptionInfo) {
        
        let state = model.getSubscriptionState(info: info)
        
        if info.subscriptionRateString == "0" {
            cell.forthSectionLabel.text = "-- %"
        }
        
        switch state {
        case .beforeSubscription:
            setBeforeSubscriptionUI(cell)
        case .duringSubscription:
            setDuringSubscriptionUI(cell)
        case .finishedSubscription:
            setFinishedSubscriptionUI(cell)
        case .notDefined:
            setNotDefinedUI(cell)
        }
    }

最後,就在 cellForRow(at:) 呼叫这个 func 就完成了

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: StockSubscriptionTableViewCell.identifier, for: indexPath) as? StockSubscriptionTableViewCell,
              let info = model.getSubscriptionInfo(at: indexPath) else {
            return UITableViewCell()
        }
        
        let state = "申购状态"
        let firstSection = "\(info.stockName) - (\(info.stockCode))"
        let secondSection = "申购股数: \(info.stockCountString)"
        let thirdSection = "申购价: \(info.actualPrice)"
        let forthSection = "中签率: \(info.subscriptionRateString) %"
        
        cell.stateLabel.text = state
        cell.firstSectionLabel.text = firstSection
        cell.secondSectionLabel.text = secondSection
        cell.thirdSectionLabel.text = thirdSection
        cell.forthSectionLabel.text = forthSection
        
        modify(cell, with: info)
        
        return cell
    }

整个 VC 的程序码如下

//
//  StockSubscriptionViewController.swift
//  ITIronMan
//
//  Created by Marvin on 2021/9/4.
//

import UIKit

class StockSubscriptionViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    private lazy var model: StockSubscriptionModel = {
        let model = StockSubscriptionModel()
        model.delegate = self
        return model
    }()

    // MARK: - life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    // MARK: - private methods
    private func setupUI() {
        tableView.dataSource = self
        tableView.delegate = self
    }
    
    // MARK: - IBAction
    @IBAction func requestSubscriptionButtonDidTap(_ sender: Any) {
        model.requestStockSubscription()
    }
    
}

extension StockSubscriptionViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return model.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: StockSubscriptionTableViewCell.identifier, for: indexPath) as? StockSubscriptionTableViewCell,
              let info = model.getSubscriptionInfo(at: indexPath) else {
            return UITableViewCell()
        }
        
        let state = "申购状态"
        let firstSection = "\(info.stockName) - (\(info.stockCode))"
        let secondSection = "申购股数: \(info.stockCountString)"
        let thirdSection = "申购价: \(info.actualPrice)"
        let forthSection = "中签率: \(info.subscriptionRateString) %"
        
        cell.stateLabel.text = state
        cell.firstSectionLabel.text = firstSection
        cell.secondSectionLabel.text = secondSection
        cell.thirdSectionLabel.text = thirdSection
        cell.forthSectionLabel.text = forthSection
        
        modify(cell, with: info)
        
        return cell
    }
    
    private func modify(_ cell: StockSubscriptionTableViewCell, with info: StockSubscriptionInfo) {
        
        let state = model.getSubscriptionState(info: info)
        
        if info.subscriptionRateString == "0" {
            cell.forthSectionLabel.text = "-- %"
        }
        
        switch state {
        case .beforeSubscription:
            setBeforeSubscriptionUI(cell)
        case .duringSubscription:
            setDuringSubscriptionUI(cell)
        case .finishedSubscription:
            setFinishedSubscriptionUI(cell)
        case .notDefined:
            setNotDefinedUI(cell)
        }
    }
    
    private func setBeforeSubscriptionUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "申购未开始"
        cell.stateLabel.textColor = .black
        cell.stateLabel.backgroundColor = .clear
    }
    
    private func setDuringSubscriptionUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "可申购"
        cell.stateLabel.textColor = .systemGreen
        cell.stateLabel.backgroundColor = .clear
    }
    
    private func setFinishedSubscriptionUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "申购结束"
        cell.stateLabel.textColor = .white
        cell.stateLabel.backgroundColor = .systemRed
    }
    
    private func setNotDefinedUI(_ cell: StockSubscriptionTableViewCell) {
        
        cell.stateLabel.text = "申购状态未定"
        cell.stateLabel.textColor = .systemGray2
        cell.stateLabel.backgroundColor = .clear
    }
}

extension StockSubscriptionViewController: StockSubscriptionModelDelegate {
    
    func didRecieveList(_ subscriptionList: [StockSubscriptionInfo], error: Error?) {
        
        if let error = error {
            print("you got error during subscriptions request: \(error.localizedDescription)")
            return
        }
        
        tableView.reloadData()
    }
}

而 UI 状态如下。

https://ithelp.ithome.com.tw/upload/images/20210920/20140622X4b8jYvddc.png

https://ithelp.ithome.com.tw/upload/images/20210920/20140622YYC2U7dxry.png

D11 完整程序码 repo

写到这里,连一根 K 线都没有,是不是和一般投资理财用的软件不同?

没问题的,下一篇开始,就会进入 K 线的制作。


<<:  D20-(9/20)-康普(4739)-特斯拉的电池正极材料供应商

>>:  [Day06] TS:整合前几天所学,来写个 Generic Functions 吧!

Day 27 | CSS Image Block Reveal Hover Effects

今天想要分享的是这个, 记得我当时看到这个效果的时候觉得真的是炫炮过头了, 马上整个影片看完做练习,...

【左京淳的JAVA WEB学习笔记】第十五章 商品上架&用户购买纪录

管理员在网站後台,需有商品上、下架操作功能。 新建BookAdd.jsp <form acti...

[DAY 29] _STM32_内部Flash

STM32晶片内部有一个Flash记忆体,主要用於储存我们所打的程序,我们在软件上打好程序编译完成後...

#28 Click! Serve! Desktop

昨天已经把大部分的 GUI 弄完了,之前也已经写好了服务器的程序,今天我们把两边拼起来吧! 搬移 s...

Apache 动态参数传递

以下可以将 www.a.com/jkhdajlf23 後缀jkhdajlf23补捉到 往後传递 而不...