iT邦幫忙

2021 iThome 鐵人賽

DAY 6
0
Mobile Development

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

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


上一篇
D5-用 Swift 和公開資訊,打造投資理財的 Apps { 實作 上市/上櫃/興櫃 所有資料的列表 }
下一篇
D7- 用 Swift 和公開資訊,打造投資理財的 Apps { 台股申購分析資料來源 }
系列文
使用 Swift 和公開資訊,打造投資理財的 Apps37

尚未有邦友留言

立即登入留言