在上一篇文章中,我們介紹了 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

這是一個(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)
}
}