iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0
Mobile Development

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

D11-用 Swift 和公開資訊,打造投資理財的 Apps { 台股申購實作.4 - 用 Calendar 物件處理台灣的民國年}

  • 分享至 

  • xImage
  •  

股票申購是和時間有關的 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 線的製作。


上一篇
D10- 用 Swift 和公開資訊,打造投資理財的 Apps { 台股申購實作.3-讓申購資訊放進可以清楚理解的 TableView }
下一篇
D12 - 用 Swift 和公開資訊,打造投資理財的 Apps { 加權指數K線圖分析 }
系列文
使用 Swift 和公開資訊,打造投資理財的 Apps37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言