Combine 與 MVVM

在上一篇文章中,我們介紹了 RxSwift 結(jié)合 MVVM 進(jìn)行 APP 開(kāi)發(fā),通過(guò) RxSwift 實(shí)現(xiàn)了數(shù)據(jù)與視圖的綁定,使 View 與 ViewModel 能夠自動(dòng)同步。

在 WWDC 2019 上,蘋(píng)果新推出了一套函數(shù)響應(yīng)式編程框架 Combine,它的設(shè)計(jì)思想借鑒于 Rx 系列,跟 RxSwift 一脈相承。今天來(lái)嘗試用 Combine 結(jié)合 MVVM,用兩種框架寫(xiě)同一個(gè) demo,對(duì)比看看 Combine 這個(gè)框架有什么不同之處。

Demo 源碼地址:https://github.com/superzcj/CombineMVVMDemo

Demo

image.png

這是一個(gè)添加中草藥的頁(yè)面,每種藥品都有數(shù)量和價(jià)格,底部匯總所選藥品的種類(lèi)、數(shù)量和總價(jià)。用戶可以增加、減少或刪除藥品。每個(gè)操作都會(huì)讓底部匯總價(jià)格信息刷新。

當(dāng)首次進(jìn)入時(shí),從后端加載默認(rèn)的藥品列表,改動(dòng)任一藥品,自動(dòng)同步底部匯總價(jià)格信息。

準(zhǔn)備

Combine 是蘋(píng)果的響應(yīng)式編程框架

Combine框架提供了一個(gè)聲明性的Swift API,用于隨時(shí)間處理值。這些值可以表示用戶界面事件,網(wǎng)絡(luò)響應(yīng),計(jì)劃事件和許多其他類(lèi)型的異步數(shù)據(jù)。

如果你熟悉 RxSwift,那么 Combine 理解起來(lái)會(huì)更容易,兩者有比較相似的概念,詳細(xì)對(duì)比可以看看以下文章
https://github.com/freak4pc/rxswift-to-combine-cheatsheet

Combine 已經(jīng)集成到開(kāi)發(fā)框架中,無(wú)需單獨(dú)引用,直接使用

import Combine

ViewModel

ViewModel 將輸入的數(shù)據(jù)轉(zhuǎn)換加工成另一種數(shù)據(jù)。在這個(gè) demo 中,頁(yè)面打開(kāi)時(shí)從后端加載默認(rèn)的列表接口,我們可以當(dāng)成一個(gè)事件 reloadDataSource,在 viewDidLoad 執(zhí)行時(shí)觸發(fā);當(dāng)點(diǎn)擊 cell 上的增加或減少按鈕時(shí),改變選擇藥品的數(shù)量,我們也可以當(dāng)成一個(gè)事件 editDrugCount;當(dāng)點(diǎn)擊 cell 上的刪除按鈕,移除這條藥品,deleteDrug 表示這樣的操作。

最終,輸入的數(shù)據(jù)包括三個(gè)事件:reloadDataSource、editDrugCount和deleteDrug。

輸出的數(shù)據(jù)有兩個(gè),items 代表藥品列表,totalCount 代表要展示的匯總價(jià)格信息。

    @Published var items: [DrugCellViewModel] = []
    @Published var totalCount: String = ""

    func reloadDataSource() 
    func editDrugCount(item: DrugCellViewModel)
    func deleteDrug(item: DrugCellViewModel)

當(dāng) reloadDataSource 事件觸發(fā)時(shí),向后端請(qǐng)求接口,拿到數(shù)據(jù)后傳給 elements。這里并沒(méi)有直接地發(fā)起接口請(qǐng)求,而是從本地記錄讀取數(shù)據(jù)。

當(dāng) editDrugCount 操作被觸發(fā)時(shí),重新生成藥品列表數(shù)據(jù),根據(jù)帶入的cell model 替換匹配到的 model,得到新的藥品列表數(shù)據(jù)。

當(dāng) deleteDrug 操作被觸發(fā)時(shí),移除帶入的 cell model,生成新的藥品列表數(shù)據(jù)。

totalCount 又是基于 items 計(jì)算而得,遍歷 items 數(shù)組內(nèi)的元素,找出藥品種類(lèi)、數(shù)量和單價(jià),從而計(jì)算出最終的匯總價(jià)格數(shù)據(jù)。


    func reloadDataSource() {
        self.request().sink { (items) in
            self.items = items
            self.totalCount = self.getTotalCount(items: items)
        }
    }
    
    func editDrugCount(item: DrugCellViewModel) {
        var arr = [DrugCellViewModel]()
        for model in self.items {
            if model.drugModel.drugId == item.drugModel.drugId {
                arr.append(item)
            } else {
                arr.append(model)
            }
        }
        self.items = arr
        self.totalCount = self.getTotalCount(items: self.items)
    }
    
    func deleteDrug(item: DrugCellViewModel) {
        if let index = self.items.firstIndex(of:item) {
            self.items.remove(at:index)
            self.totalCount = self.getTotalCount(items: self.items)
        }
    }

    func getTotalCount(items: [DrugCellViewModel]) -> String {
        var sum = 0;
        var priceSum = 0.00;

        for cellViewModel in items {
            sum += cellViewModel.drugModel.drugCount
            let price : Double =  (cellViewModel.drugModel.maxPrice) * Double(cellViewModel.drugModel.drugCount)
            priceSum += price
        }
        return "共\(items.count)味藥,\(sum)g,藥品參考總價(jià):\(priceSum)元"
    }

ViewController

在 ViewController 中,我們用一個(gè) tableView 展示藥品列表。

首先定義一個(gè)變量 DrugViewModel,并在 viewDidLoad 方法中,初始化視圖和數(shù)據(jù)綁定

    var viewModel: DrugViewModel = DrugViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        configView()
        setupViewModel()
        
    }

根據(jù)用戶的三個(gè)操作事件:reloadDataSource、editDrugCount 和 deleteDrug,綁定 tableView 和 viewModel。 items 綁定到 tableView 上,totalCount 綁定到底部 label 上。

func setupViewModel() {
        
        viewModel.$items.receive(on: RunLoop.main).sink { items in
            self.cellViewModels = items
            self.tableView.reloadData()
        }
        viewModel.$totalCount.receive(on: RunLoop.main).sink { value in
            self.titleLabel.text = value
        }
        
        viewModel.reloadDataSource()
    }

配置藥品 cell 時(shí),cell 有兩個(gè)回調(diào),增加或減少數(shù)量回調(diào)和刪除回調(diào),這兩個(gè)回調(diào)觸發(fā)時(shí),分別向 viewModle 發(fā)送 editDrugCount 和 deleteDrug 操作事件。

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return cellViewModels.count;
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.DrugTableViewCell, for: indexPath)
       
        if let cell = cell as? DrugTableViewCell {
            let rowViewModel = cellViewModels[indexPath.row]
            print(rowViewModel)
            cell.setup(viewModel: rowViewModel)
            rowViewModel.editPublisher.receive(on: RunLoop.main).sink { vm in
                self.viewModel.editDrugCount(item: vm)
            }
            
            rowViewModel.deletePublisher.receive(on: RunLoop.main).sink { vm in
                self.viewModel.deleteDrug(item: vm)
            }
            
        }
        return cell
    }

CellViewModel 和 Cell

對(duì)于藥品 Cell,持有一個(gè)屬性 DrugCellViewModel,在初始化時(shí),把 cellViewModel 與 View 進(jìn)行綁定

    private var viewModel: DrugCellViewModel?
    public func setup(viewModel:DrugCellViewModel?) {
        self.viewModel = viewModel
        configureView()
    }
    
    private func configureView() {
        guard let viewModel = viewModel else { return }
        drugNameLabel.text = viewModel.drugModel.drugName
        textField.text = "\(viewModel.drugModel.drugCount)"
        drugPriceLabel.text = "\(viewModel.drugModel.maxPrice)元"
    }

DrugCellViewModel 擁有一個(gè)該Cell 對(duì)應(yīng)的 DrugModel,在接收到從 ViewController 傳來(lái)的事件時(shí),修改 DrugModel 并傳遞回調(diào)事件。

為了簡(jiǎn)化異步事件回調(diào),這里使用 AnyPublisher 包裝對(duì)象并使用PassthroughSubject 產(chǎn)生異步事件。以 editPublisher 為例,當(dāng)用戶點(diǎn)擊增加或減少藥品數(shù)量按鈕時(shí),editHeadingPublisher 發(fā)送一個(gè)事件,參數(shù)為自身。

class DrugCellViewModel: NSObject {
    
    var drugModel: DrugModel
    
    private let editHeadingPublisher: PassthroughSubject<DrugCellViewModel, Never>
    var editPublisher: AnyPublisher<DrugCellViewModel, Never>

    private let deleteHeadingPublisher: PassthroughSubject<DrugCellViewModel, Never>
    var deletePublisher: AnyPublisher<DrugCellViewModel, Never>

    
    init(drugModel:DrugModel) {
        self.drugModel = drugModel
        
        editHeadingPublisher = PassthroughSubject<DrugCellViewModel, Never>()
        editPublisher = editHeadingPublisher.eraseToAnyPublisher()
        
        deleteHeadingPublisher = PassthroughSubject<DrugCellViewModel, Never>()
        deletePublisher = deleteHeadingPublisher.eraseToAnyPublisher()
    }
    
    func addDrug() {
        self.drugModel.drugCount += 1
        
        editHeadingPublisher.send(self)
    }

    func minusDrug() {
        if drugModel.drugCount > 0 {
            self.drugModel.drugCount += 1
            editHeadingPublisher.send(self)
        }
    }

    func deleteDrug(){
        deleteHeadingPublisher.send(self)
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容