iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Mobile Development

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

D5-用 Swift 和公開資訊,打造投資理財的 Apps { 實作 上市/上櫃/興櫃 所有資料的列表 }

寫到第五天,開始寫 UI 囉~~

前面都是在做資料處理,所以只有程式碼,沒有 UI 畫面,謝謝看到今天的朋友

台股光是上市的家數是超過1000 家,是不可能在一個手機上顯示所有公司的基本資料的,在手機上,我們常使用 UITableView 來呈現大數量,且格式相近的資料。

依照 Apple MVC 的框架,每個人的角色分配如下

Model: 負責處理邏輯,不會直接和 View 進行溝通

View: 負責呈現資料,不會直接和 Model 進行溝通

Controller: 成為 Model 和 View 的中間人,當有 View 需要資料的時候,負責提供資料。如果 View 被點擊,則處理後續的點擊事件。當 Model 收到資料時,Controller 成為 Model 通知的對象。**

https://ithelp.ithome.com.tw/upload/images/20210914/20140622VkyZssdFX4.jpg

這個頁面現在要呈現 上市/上櫃/興櫃 的公司基本資料,而這些基本資料需要從雲端下載。那這個頁面的 MVC 職責大概是這樣。

**Model: 負責下載資料,並儲存下載後的資料。在實際專案的時候,通常還會針對這種不會馬上變化的資料,進行快取。但這邊因為不是 key feature,所以不進行快取的實作。但如果要做的話,把資料放在 UserDefaults 或是 CoreData 裡面就可以做到了。

View: 在 VC 的 RootView 下,主要呈現公司基本資料的列表。但設計上,我不想要在 VC 的生命週期中直接發動 URLRequest,這一段,我希望用 button 的 action 來發動。第一階段希望做成用 button 來發動下載,這樣比較容易說明每個動作,也比較容易解說每一個 response 後做的行為。而 TableViewCell 裡,想呈現的資訊是,股票名,股票代號,資本額。

Controller: 在生命週期中,並不特別做什麼。但是當 button 按下的時候,會去呼叫 model 進行對應的資料下載。當 model 資料有變更的時候,會呼叫 tableView.reloadData(),去更新列表。

基本的 data model 設計,這邊會需要 conform Hashable,是因為我習慣用 Set 在更新的時候進行資料的合併。

import Foundation

struct StockBasicInfo: Hashable {
    
    let stockCode: String
    let stockName: String
    let companyName: String
    let capital: String
}

基本的 UI layout 設計如下。

https://ithelp.ithome.com.tw/upload/images/20210914/20140622Lx25ZHcRad.png

創建一個 RequestBasicInfoViewController

import UIKit

class RequestBasicInfoViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

    }
}

先在一開始的 LandingViewController 拉一個 button,會發動 navigationController push 轉場

@IBAction func pushRequestBasicInfoVC(_ sender: Any) {
        
        let storyboard = UIStoryboard(name: "RequestBasicInfo", bundle: nil)
        if let vc = storyboard.instantiateViewController(withIdentifier: "RequestBasicInfoViewController") as? RequestBasicInfoViewController {
            
            navigationController?.pushViewController(vc, animated: true)
        }
    }

接下來,進行 Model 的實作,這邊我選擇使用 delegate pattern 來通知 VC 資料已經下載好了。就前面所述,Model 要處理邏輯,需要處理的部分如下。

  • 上市公司基本資料下載
  • 上櫃公司基本資料下載
  • 興櫃公司基本資料下載
  • 列表總共數量
  • 第 n 個列表的資料是哪家公司?
enum MarketType: String {
    case twStock = "上市"
    case otc = "上櫃"
    case emerging = "興櫃"
}
import Foundation

protocol RequestBasicInfoModelDelegate: AnyObject {
    func didRecieveCompanyInfo(_ companyList: [StockBasicInfo], error: Error?)
}

class RequestBasicInfoModel {
    
    weak var delegate: RequestBasicInfoModelDelegate?
    
    private var recievedInfo = [MarketType]()
    
    private var companyList = [StockBasicInfo]()
    
    var count: Int {
        return companyList.count
    }
    
    private lazy var stockInfoManager: StockInfoManager = {
        let manager = StockInfoManager()
        return manager
    }()
    
    func getStockInfo(at indexPath: IndexPath) -> StockBasicInfo? {
        
        let index = indexPath.row
        if companyList.indices.contains(index) {
            return companyList[index]
        }
        
        return nil
    }
    
    func requestTwStock() {
        
        if recievedInfo.contains(.twStock) {
            print("已經拿過資料")
            return
        }
        
        stockInfoManager.requestTwStockCodeAndName { [weak self] list, error in
            self?.updateStockInfo(from: list, marketType: .twStock)
            self?.delegate?.didRecieveCompanyInfo(list, error: error)
        }
    }
    
    func requestOTCStock() {
        
        if recievedInfo.contains(.otc) {
            print("已經拿過資料")
            return
        }
        
        stockInfoManager.requestOTCCodeAndName { [weak self] list, error in
            self?.updateStockInfo(from: list, marketType: .otc)
            self?.delegate?.didRecieveCompanyInfo(list, error: error)
        }
    }
    
    func requestEmergingStock() {
        
        if recievedInfo.contains(.emerging) {
            print("已經拿過資料")
            return
        }
        
        stockInfoManager.requestEmerginCodeAndName { [weak self] list, error in
            self?.updateStockInfo(from: list, marketType: .emerging)
            self?.delegate?.didRecieveCompanyInfo(list, error: error)
        }
    }
    
    private func updateStockInfo(from list: [StockBasicInfo], marketType: MarketType) {
        
        recievedInfo.append(marketType)
        
        let recievedList = Set(list)
        let updatedList = Set(companyList).union(recievedList)
        
        companyList = Array(updatedList).sorted { $0.stockCode < $1.stockCode }
    }
}

UITableView 的程式碼,這邊 custom 一個 TableViewCell,CompanyBasicInfoTableViewCell

class CompanyBasicInfoTableViewCell: UITableViewCell {

    static let identifier = "CompanyBasicInfoTableViewCell"
    
    @IBOutlet weak var codeAndNameLabel: UILabel!
    
    @IBOutlet weak var capitalLabel: UILabel!
}

UITableView 如果設置上有遇到困難,這邊有 Apple 文件
Apple 對於 UITableView 的說明文件

ViewController 部分的程式碼

import UIKit

class RequestBasicInfoViewController: UIViewController {
    
    @IBOutlet weak var stateLabel: UILabel!
    
    @IBOutlet weak var tableView: UITableView!
    
    private lazy var model: RequestBasicInfoModel = {
        let model = RequestBasicInfoModel()
        model.delegate = self
        return model
    }()
    
    // MARK: - life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    // MARK: - private methods
    private func setupUI() {
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    // MARK: - IBAction
    @IBAction func requestTwStockButtonDidTap(_ sender: Any) {
        
        model.requestTwStock()
    }
    
    @IBAction func requestOTCButtonDidTap(_ sender: Any) {
        
        model.requestOTCStock()
    }
    
    @IBAction func requestEmergingButtonDidTap(_ sender: Any) {
        
        model.requestEmergingStock()
    }
}

extension RequestBasicInfoViewController: 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: CompanyBasicInfoTableViewCell.identifier, for: indexPath) as? CompanyBasicInfoTableViewCell,
              let info = model.getStockInfo(at: indexPath) else {
            return UITableViewCell()
        }
        
        let codeName = "\(info.stockName) - (\(info.stockCode))\n\(info.companyName)"
        
        let capital = "資本額: \(info.capital) 元"
        
        cell.codeAndNameLabel.text = codeName
        cell.capitalLabel.text = capital
        
        return cell
    }
}

extension RequestBasicInfoViewController: RequestBasicInfoModelDelegate {
    
    func didRecieveCompanyInfo(_ companyList: [StockBasicInfo], error: Error?) {
        
        if let error = error {
            
            print("basic info reqeust got error: \(error.localizedDescription)")
            return
        }
        
        updateStateUI()
        
        tableView.reloadData()
    }
    
    private func updateStateUI() {
        
        var recievedMarketsText = ""
        
        for market in model.recievedInfo {
            recievedMarketsText += "\(market.rawValue)  "
        }
        
        stateLabel.text = "已取得 \(recievedMarketsText) 資料 - 數量 \(model.count) 筆"
    }
}

完成後的狀態

剛進入頁面

https://ithelp.ithome.com.tw/upload/images/20210914/20140622COyhAUMLMD.png

完成下載 上市公司資料

https://ithelp.ithome.com.tw/upload/images/20210914/20140622SsIYdSMaPK.png

完成下載 上櫃公司資料

https://ithelp.ithome.com.tw/upload/images/20210914/20140622U7A01Mo9gO.png

完成下載 興櫃公司資料

https://ithelp.ithome.com.tw/upload/images/20210914/20140622JMnCM8stZt.png

延伸功能:

UITableView 如果設置上有遇到困難,這邊有 Apple 文件
Apple 對於 UITableView 的說明文件

通常在列表的 UI 上,點擊後,會再推入一個 VC 去呈現這個格子內容的詳細資料。但這邊的 demo 只會做到這裡,詳細頁的 UI 在實作並不是難度很大的功能,比較難的是資料來源。

如果要實作,就在 UITableViewDelegate 的 tableView(_:didSelectRowAt:) 實作拿出 info model,然後把 info model 傳入下一個 vc 即可。

D5 程式碼可以在 GitHub 上下載的到
GitHub Repo


上一篇
D4-用 Swift 和公開資訊,打造投資理財的 Apps { 下載公司股票代號和股票名稱等基本資料 }
下一篇
D6-用 Swift 和公開資訊,打造投資理財的 Apps { 加上 filter,實作搜尋 上市/上櫃 功能 }
系列文
使用 Swift 和公開資訊,打造投資理財的 Apps37

尚未有邦友留言

立即登入留言