D6-用 Swift 和公开资讯,打造投资理财的 Apps { 加上 filter,实作搜寻 上市/上柜 功能 }

列表的确是在有限萤幕空间中,呈现大量资料的一个手法。但从前一篇可以知道,你这个列表会有数千笔资料的时候,找出某一笔或是某一群你想要的资料,就变得不容易了。

这个问题的解法,可以看 iPhone 内建的软件,像是通讯录、备忘录的设计。这些 App 在上方都有个文字输入框,这些输入框在里面有文字的时候,会对内容进行 filter,把含有输入的文字的资料呈现。

所以这个 VC 会有两种状态

  • 输入框内没有文字(下称状态1) - 列表呈现所有资料
  • 输入框有文字(下称状态2) - 列表只呈现属性中含有输入框文字的资料

那再回到 MVC 的架构下,我们的 Model 就要有状态的差别。不用更动的部分,就是下载那一部分的逻辑。而要能分出状态 1、2,我选择使用 Bool 来做切换,所以 Model 中要加一个 isFiltering = false。

而在原来的 companyList 以外,还要再加一个 filtedList,让状态 2 下的 controller,使用这一部分的资料。

private var companyList = [StockBasicInfo]()
    
private var filtedList = [StockBasicInfo]()

而在 count 的回传值,会依照现在的状态不同,而回传 controller 所需要的值。

var count: Int {
        
        if isFiltering {
            return filtedList.count
        }
        return companyList.count
    }

而在原来拿取的 data model 的方法,因为有两个状态,所以再写出两个 private func ,让 model 在不同的状态下,呼叫不同的 func 拿出该拿的 data model

private func getCompanyFromAll(at indexPath: IndexPath) -> StockBasicInfo? {
        
        let index = indexPath.row
        
        if companyList.indices.contains(index) {
            return companyList[index]
        }
        return nil
    }
    
    private func getCompanyFromFilter(at indexPath: IndexPath) -> StockBasicInfo? {
        
        let index = indexPath.row
        
        if filtedList.indices.contains(index) {
            return filtedList[index]
        }
        return nil
    }
    
    func getStockInfo(at indexPath: IndexPath) -> StockBasicInfo? {
        
        if isFiltering {
            return getCompanyFromFilter(at: indexPath)
        } else {
            return getCompanyFromAll(at: indexPath)
        }
    }

整个 Model 的程序码如下

//
//  RequestBasicInfoModel.swift
//  ITIronMan
//
//  Created by Marvin on 2021/9/3.
//

import Foundation

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

class RequestBasicInfoModel {
    
    weak var delegate: RequestBasicInfoModelDelegate?
    
    private var isFiltering = false
    
    var filterText = "" {
        didSet {
            updateFilteringState()
        }
    }
    
    var recievedInfo = [MarketType]()
    
    private var companyList = [StockBasicInfo]()
    
    private var filtedList = [StockBasicInfo]()
    
    var count: Int {
        
        if isFiltering {
            return filtedList.count
        }
        return companyList.count
    }
    
    private lazy var stockInfoManager: StockInfoManager = {
        let manager = StockInfoManager()
        return manager
    }()
    
    private func updateFilteringState() {
        
        if filterText.count > 0 {
            isFiltering = true
            filtedList = companyList.filter({ basicInfo in
                
                return basicInfo.stockCode.contains(filterText) ||
                    basicInfo.stockName.contains(filterText) ||
                    basicInfo.companyName.contains(filterText)
            })
        } else {
            isFiltering = false
        }
        
        delegate?.didUpdateFiltedList()
    }
    
    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 }
    }
    
    private func getCompanyFromAll(at indexPath: IndexPath) -> StockBasicInfo? {
        
        let index = indexPath.row
        
        if companyList.indices.contains(index) {
            return companyList[index]
        }
        return nil
    }
    
    private func getCompanyFromFilter(at indexPath: IndexPath) -> StockBasicInfo? {
        
        let index = indexPath.row
        
        if filtedList.indices.contains(index) {
            return filtedList[index]
        }
        return nil
    }
    
    func getStockInfo(at indexPath: IndexPath) -> StockBasicInfo? {
        
        if isFiltering {
            return getCompanyFromFilter(at: indexPath)
        } else {
            return getCompanyFromAll(at: indexPath)
        }
    }
    
    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)
        }
    }
}

接下来,进行 View 的 UI元件追加。在 Storyboard 上,加上一个 UITextField。

https://ithelp.ithome.com.tw/upload/images/20210915/20140622Qanuc5NZJF.png

然後,将 setupUI() 的时候,将 UITextField 绑上 action。只要 TextField 的值有变化,就将值传 Model,接下来的逻辑,就是 Model 该负责处理的。而当 filter 状态有变化的时候,Controller 就负责让 tableView.reloadData() 刷新 View。

整个 VC 的程序码如下

//
//  RequestBasicInfoViewController.swift
//  ITIronMan
//
//  Created by Marvin on 2021/9/3.
//

import UIKit

class RequestBasicInfoViewController: UIViewController {
    
    @IBOutlet weak var stateLabel: UILabel!
    
    @IBOutlet weak var tableView: UITableView!
    
    @IBOutlet weak var filterTextField: UITextField!
    
    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
        filterTextField.addTarget(self, action: #selector(filterStock), for: .editingChanged)
    }
    
    @objc private func filterStock() {
        
        if filterTextField.hasText,
           let text = filterTextField.text {
            
            model.filterText = text
        } else {
            model.filterText = ""
        }
    }
    
    // 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 didUpdateFiltedList() {
        tableView.reloadData()
    }
    
    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) 笔"
    }
}

程序码下载
GitHub Repo


<<:  D15-(9/15)-南亚(1303)-也是存股好选择的台塑四宝

>>:  day 8 - 程序码也要断舍离

[Day18] 团队管理:OKR & OGSM

OKR 目标影响我们的可能性与看事情的视角 OKR,目标与关键成果(Objective and Ke...

Chapter2 - Canvas动画(I)玩转路径和位移 动画原来这麽简单

这个章节呢,同样会以实作为主,在解决问题中带大家学习,逐渐引入JS的语言特性,前面一样会从简单的开始...

【JavaScript】==和===

【前言】 本系列为个人前端学习之路的学习笔记,在过往的学习过程中累积了很多笔记,如今想藉着IT邦帮忙...

Day12 - Google Kubernetes Engine 基础 - Pod 建置

前言 前一天我们建立好了 Kubernetes 的环境,今天就来实际使用看看,将应用程序透过 Pod...

#4. Covid 19 Tracker(Vue版)

今天任务的实作内容,主要是参考这支影片 影片中使用的程序码风格是Vue的Option API,在我的...