當(dāng)我們的APP交互復(fù)雜、邏輯復(fù)雜時围肥,ViewController 就會變得十分臃腫奇瘦,大量的代碼填充其中,使得 ViewController 承擔(dān)的職責(zé)過多倦西。臃腫的 ViewController 難以理解,難以維護赁严,難以擴展扰柠,增加了后續(xù)開發(fā)的復(fù)雜度,降低了整體開發(fā)的效率疼约。
現(xiàn)在流行的解決方案是 MVVM 架構(gòu)卤档,它在 MVC 的基礎(chǔ)上引入了 ViewModel,數(shù)據(jù)展示程剥、樣式定制等數(shù)據(jù)轉(zhuǎn)換操作就移到其中裆装。這樣 ViewController 只需要負(fù)責(zé)數(shù)據(jù)調(diào)配,承擔(dān)的職責(zé)減輕了,臃腫的情況自動就消失了哨免。
RxSwift 提供了綁定機制茎活,讓數(shù)據(jù)與視圖自動同步,一方有變化琢唾,自動通知另一方更新载荔,避免了編寫大量繁瑣的命令式綁定代碼。
下面我們寫一個 demo采桃,看看 RxSwift 結(jié)合 MVVM 是如何為 ViewController 瘦身的懒熙。
源碼地址:https://github.com/superzcj/RxSwiftMVVMDemo
Demo
這是一個添加中草藥的頁面,每種藥品都有數(shù)量和價格普办,底部匯總所選藥品的種類工扎、數(shù)量和總價。用戶可以增加衔蹲、減少或刪除藥品肢娘。每個操作都會讓底部匯總價格信息刷新。
當(dāng)首次進入時舆驶,從后端加載默認(rèn)的藥品列表橱健,改動任一藥品,自動同步底部匯總價格信息沙廉。
準(zhǔn)備
首先拘荡,把 RxSwift 添加到項目中。我們通常使用 Cocoapods 管理第三方依賴庫撬陵。在 pod 文件中珊皿,添加以下代碼:
pod 'RxSwift'
pod 'RxCocoa'
pod 'RxDataSources'
RxCocoa 是 RxSwift 的一部分,主要是 UI 相關(guān)控件的 Rx 封裝巨税,如控件的綁定功能蟋定。RxDataSources包括與UITableView和UICollectionView相關(guān)的綁定功能。
ViewModel
ViewModel 將輸入的數(shù)據(jù)轉(zhuǎn)換加工成另一種數(shù)據(jù)垢夹。在這個 demo 中,頁面打開時從后端加載默認(rèn)的列表接口维费,我們可以當(dāng)成一個事件 reload果元,在 viewDidLoad執(zhí)行時觸發(fā);當(dāng)點擊 cell 上的增加或減少按鈕時犀盟,改變選擇藥品的數(shù)量而晒,我們也可以當(dāng)成一個事件 editTrigger;當(dāng)點擊 cell 上的刪除按鈕阅畴,移除這條藥品倡怎,deleteTrigger 表示這樣的操作。最終,輸入的數(shù)據(jù)包括三個事件:reload监署、editTrigger和deleteTrigger颤专。
這三個事件都是 PublishSubject 類型,作為一個可監(jiān)聽序列钠乏,當(dāng)監(jiān)聽到事件觸發(fā)時栖秕,做出響應(yīng)。
輸出的數(shù)據(jù)有兩個晓避,items 代表藥品列表簇捍,totalCount 代表要展示的匯總信息。
BehaviorRelay 是一個序列俏拱,它有一個value 屬性暑塑,通過這屬性能拿到最新的值。而通過它的 accept() 方法可以對值進行修改锅必,并能夠?qū)⑿薷牡闹蛋l(fā)送出去事格。Driver 是一個特別的序列,它是為了簡體 UI 層的代碼况毅,它不會產(chǎn)生 error 事件分蓖,且一定在 MainScheduler 監(jiān)聽。
struct Input {
let reload: PublishSubject<[DrugCellViewModel]>
let editTrigger: PublishSubject<DrugCellViewModel>
let deleteTrigger: PublishSubject<DrugCellViewModel>
}
struct Output {
let items: BehaviorRelay<[DrugCellViewModel]>
let totalCount: Driver<String>
}
transform 方法將輸入轉(zhuǎn)換成輸出尔许,首先定義一個BehaviorRelay類型的序列么鹤,用于存儲藥品列表數(shù)據(jù)。當(dāng) input.reload 事件觸發(fā)時味廊,向后端請求接口蒸甜,拿到數(shù)據(jù)后傳給 elements。當(dāng) input.editTrigger 操作被觸發(fā)時余佛,重新生成藥品列表數(shù)據(jù)柠新,根據(jù)帶入的cell model替換匹配到的model,得到新的藥品列表數(shù)據(jù)辉巡。當(dāng) input.deleteTrigger 操作被觸發(fā)時恨憎,移除帶入的 cell model,生成新的藥品列表數(shù)據(jù)郊楣。
totalCount 又是基于 elements 計算而得憔恳,遍歷 elements 數(shù)組內(nèi)的元素,找出藥品種類净蚤、數(shù)量和單價钥组,從而計算出最終的匯總價格數(shù)據(jù)。
func transform(input: Input) -> Output {
let elements = BehaviorRelay<[DrugCellViewModel]>(value: [])
input.reload.flatMapLatest({ (item) -> Observable<[DrugCellViewModel]> in
return self.request()
}).subscribe(onNext: { (items) in
elements.accept(items)
}).disposed(by: self.disposeBag)
input.editTrigger.subscribe(onNext: { (item) in
var arr = [DrugCellViewModel]()
for model in elements.value {
if model.drugId == item.drugId {
arr.append(item)
} else {
arr.append(model)
}
}
elements.accept(arr)
}).disposed(by: self.disposeBag)
input.deleteTrigger.subscribe(onNext: { (item) in
if let index = elements.value.firstIndex(of:item) {
var arr = elements.value
arr.remove(at:index)
elements.accept(arr)
}
}).disposed(by: self.disposeBag)
let totalCount = elements.map({ (items) -> String in
var sum = 0;
var priceSum = 0.00;
for cellViewModel in items {
sum += cellViewModel.drugCount
let price : Double = (cellViewModel.maxPrice) * Double(cellViewModel.drugCount)
priceSum += price
}
return "共\(items.count)味藥今瀑,\(sum)g程梦,藥品參考總價:\(priceSum)元"
}).asDriver(onErrorJustReturn: "")
return Output(items: elements, totalCount: totalCount)
}
ViewController
在 ViewController 中点把,我們使用 RxSwift 綁定 tableView 數(shù)據(jù)源。
首先定義一個變量 DrugViewModel屿附,并在 viewDidLoad 方法中郎逃,初始化視圖和數(shù)據(jù)綁定
var viewModel: DrugViewModel = DrugViewModel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
configView()
setupViewModel()
reloadSubject.onNext([])
}
根據(jù)用戶的三個操作事件:reloadSubject、editTrigger 和 deleteTrigger拿撩,得到輸出數(shù)據(jù) output衣厘。然后將 output.items 綁定到 tableView 上,output.totalCount 綁定到底部 label 上压恒。
配置藥品 cell 時影暴,cell 有兩個回調(diào),增加或減少數(shù)量回調(diào)和刪除回調(diào)探赫,這兩個回調(diào)觸發(fā)時型宙,分別向 viewModle 發(fā)送 editSubject 和 deleteTrigger 操作信號。
func setupViewModel() {
let input = DrugViewModel.Input(reload: reloadSubject, editTrigger: editSubject, deleteTrigger: deleteTrigger )
let output = viewModel.transform(input: input)
output.totalCount.drive(onNext: { (content) in
self.titleLabel.text = content
}).disposed(by: disposeBag)
output.items.map({ (items) -> [DrugCellViewModel] in
self.cellViewModels = items
return items
}).asDriver(onErrorJustReturn: []).drive(tableView.rx.items(cellIdentifier: CellIdentifiers.DrugTableViewCell, cellType: DrugTableViewCell.self)) { index, viewModel, cell in
let rowViewModel = self.cellViewModels[index]
cell.setup(viewModel: rowViewModel)
rowViewModel.numberButtonTapped.asObserver().subscribe(onNext: { (drugCellViewModel) in
self.editSubject.onNext(drugCellViewModel)
}).disposed(by: self.disposeBag)
rowViewModel.deleteButtonTapped.asObserver().subscribe(onNext: { (drugCellViewModel) in
self.deleteTrigger.onNext(drugCellViewModel)
}).disposed(by: self.disposeBag)
}.disposed(by: self.disposeBag)
}
CellViewModel 和 Cell
對于這個藥品列表中的 Cell伦吠,它持有一個屬性 DrugCellViewModel妆兑,在初始化時,把 cellViewModel 與 View 進行綁定
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.drugName
textField.text = "\(viewModel.drugCount)"
drugPriceLabel.text = "\(viewModel.maxPrice)元"
}
DrugCellViewModel 擁有一個該 Cell 對應(yīng)的 DrugModel毛仪,在接收到用戶點擊事件時搁嗓,修改 DrugModel 并傳遞回調(diào)事件。
numberButtonTapped 和 deleteButtonTapped 是兩個回調(diào)事件箱靴,在 Cell 上的按鈕被點擊時觸發(fā)腺逛,參數(shù)為自身。
class DrugCellViewModel: NSObject {
var drugModel: DrugModel
var drugCount:Int
var numberButtonTapped = PublishSubject<DrugCellViewModel>()
var deleteButtonTapped = PublishSubject<DrugCellViewModel>()
init(drugModel:DrugModel) {
self.drugModel = drugModel
drugCount = drugModel.drugCount
}
func addDrug() {
self.drugModel.drugCount += 1
drugCount+=1
numberButtonTapped.onNext(self)
}
func minusDrug() {
if drugCount > 0 {
drugCount-=1
self.drugModel.drugCount -= 1
}
numberButtonTapped.onNext(self)
}
func deleteDrug(){
deleteButtonTapped.onNext(self)
}
}