列表的確是在有限螢幕空間中,呈現大量資料的一個手法。但從前一篇可以知道,你這個列表會有數千筆資料的時候,找出某一筆或是某一群你想要的資料,就變得不容易了。
這個問題的解法,可以看 iPhone 內建的軟體,像是通訊錄、備忘錄的設計。這些 App 在上方都有個文字輸入框,這些輸入框在裡面有文字的時候,會對內容進行 filter,把含有輸入的文字的資料呈現。
所以這個 VC 會有兩種狀態
那再回到 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。
然後,將 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