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


<<:  30 天 React 学习之路 (Day1)

>>:  Kneron - 在Raspberry Pi 4(Raspbian Buster)上安装 OpenCV 参考笔记

[Day 5] 认识Git,做好版本控制

前言 什麽是Git,刚开始认识它的时候,以为他跟GitHub 有着什麽关系或是某个简称,结果两着是各...

乙级电脑软件设计技术士-Java 考照历程

在网路上很少关於这个科目的介绍,虽然第一次盲考就通过,但是整个过程还是如履薄冰。因此留下这次考试的过...

【Day21】什麽是函式?

函式功能 函式在 JavaScript 中为物件型别,以下列出它一般的物件差别 被呼叫的能力 {} ...

Day 21 Compose UI Animation III

今年的疫情蛮严重的,希望大家都过得安好,希望疫情快点过去,能回到一些线下技术聚会的时光~ 今天目标:...

恶意程序-伴侣病毒( malicious program-Companion virus)

-主引导记录(MBR)和引导扇区(来源:Syed Fahad) .该多态病毒沉思修改整个系统,这样...