理解 RxSwift:MVVM(三)

當(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

image.png

這是一個添加中草藥的頁面,每種藥品都有數(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)
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衡怀,一起剝皮案震驚了整個濱河市棍矛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抛杨,老刑警劉巖够委,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怖现,居然都是意外死亡茁帽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門屈嗤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來潘拨,“玉大人,你說我怎么就攤上這事恢共≌角铮” “怎么了璧亚?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵讨韭,是天一觀的道長脂信。 經(jīng)常有香客問我,道長透硝,這世上最難降的妖魔是什么狰闪? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮濒生,結(jié)果婚禮上埋泵,老公的妹妹穿的比我還像新娘。我一直安慰自己罪治,他們只是感情好丽声,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著觉义,像睡著了一般雁社。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晒骇,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天霉撵,我揣著相機與錄音,去河邊找鬼洪囤。 笑死徒坡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瘤缩。 我是一名探鬼主播喇完,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼款咖!你這毒婦竟也來了何暮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤铐殃,失蹤者是張志新(化名)和其女友劉穎海洼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體富腊,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡坏逢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了赘被。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片是整。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖民假,靈堂內(nèi)的尸體忽然破棺而出浮入,到底是詐尸還是另有隱情,我是刑警寧澤羊异,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布事秀,位于F島的核電站彤断,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏易迹。R本人自食惡果不足惜宰衙,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望睹欲。 院中可真熱鬧供炼,春花似錦、人聲如沸窘疮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闸衫。三九已至先嬉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間楚堤,已是汗流浹背疫蔓。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留身冬,地道東北人衅胀。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像酥筝,于是被迫代替她去往敵國和親滚躯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354