MVVM+RxSwift

前言

之前對MVVM的理解和運用以爲很淺薄,在項目中用處只是對ViewController減負git

  1. 沒有作數據與View的綁定,沒有作到真正的數據驅動視圖
  2. 沒有體現出MVVM易於測試的好處
  3. 對於RxSwift的運用也僅限於網絡請求庫,RxCocoa的一些優勢沒有運用到項目

因此是時候在項目中使用真正的MVVM了(整理出套路代碼),介於項目中已經引入了RxSwift,因此就用它來實現了,在學習本文前可能會要求讀者對RxSwift有必定的瞭解和使用。github

MVVM架構圖

*MVVM*架構圖.png
在ViewController 裏將數據源綁定到對應的View,這裏只是單向綁定,在ViewModel進行網絡請求等改變數據行爲的操做更新Model,再由ViewModel通知View更新。至於怎麼實現數據綁定的,下面會詳細說明。

MVVM目錄結構

image.png

上圖是項目中的一個模塊,使用MVVM架構後的文件結構,Model被我集中的定義在一個公共的文件夾裏了,接下來我會詳細介紹。swift

ViewModel

查閱了許多資料,不一樣人對ViewModel的實現有不少種,我這裏總結了一下多數人也是我比較贊同的一種實現方法 網絡

image.png
將ViewModel理解爲一個簡單的黑盒子,它接受輸入以產生輸出,這裏的輸入和輸出都是一個個序列。這樣就能實現MVVM的最大的好處,使業務邏輯可測試。ViewModel裏面主要進行網絡請求、業務處理等操做。網絡請求的框架咱們用的是Moya,由於它可使咱們的請求獲得一個序列,而後咱們才能夠進行數據綁定。 通常的ViewModel大概是長這樣的:

class ViewModel {
    // 輸入轉化輸出,這裏是真正的業務邏輯代碼了
    func transform(input: Input) -> Output {
    }
}
extension ViewModel {
    // 輸入,類型是Driver,由於跟UI控件有關
    struct Input {
    }
    // 輸出,類型也是Driver
    struct Output {
    }
}
複製代碼

Model

對於Model,它主要是定義一些數據模型,固然你也能夠封裝一些數據轉換等公共的業務方法。架構

ViewController和View

ViewController的主要做用是管理視圖的生命週期,綁定數據和View的關係,數據綁定的實現主要是經過RxDataSources+RxSwift來實現的,因此說你的項目中要引入這兩個庫。RxCocoa給UI框架提供了Rx支持,讓咱們可以使用按鈕點擊序列,這樣咱們就能夠給ViewModel提供輸入了,而RxDataSources可以幫助你簡化書寫 TabelView或 CollectionView的數據源這一過程,而且提供了經過序列更新TableView的方法,這時候咱們只要把ViewModel的數據輸出序列綁定到TableView的數據源序列就能夠了。框架

Navigator

Navigator是從ViewController剝離出來用來控制視圖跳轉學習

上代碼

下圖是上述目錄結構中一個頁面 測試

291549013399_.pic.jpg

先分析下界面上的輸入和輸出ui

輸入:進入頁面時的請求,重命名按鈕點擊,刪除按鈕點擊,新建分組按鈕點擊spa

輸出:TableView數據源,頁面Loading狀態

ViewModel核心代碼:

class MenuSubGroupViewModel {
    func transform(input: Input) -> Output {
        let loadingTracker = ActivityIndicator()
        let createNewGroup = input.createNewGroup
            .flatMapLatest { _ in
                self.navigator.toMenuEditGroupVC()
                    .saveData
                    .asDriverOnErrorJustComplete()
            }
        let renameGroup = input.cellRenameButtonTap
            .flatMapLatest...
        let getMenusInfo = Driver.merge(createNewGroup, input.viewDidLoad, renameGroup)
            .flatMapLatest...
        let deleteSubGroups = input.cellDeleteButtonTap
            .flatMapLatest...
        let dataSource = Driver.merge(getMenusInfo, deleteSubGroups)
        let loading = loadingTracker.asDriver()
        return Output(dataSource: dataSource, loading: loading)
    }
}
extension MenuSubGroupViewModel {
    struct Input {
        let createNewGroup: Driver<Void>
        let viewDidLoad: Driver<Void>
        let cellDeleteButtonTap: Driver<IndexPath>
        let cellRenameButtonTap: Driver<IndexPath>
    }
    struct Output {
        let dataSource: Driver<[MenuSubGroupViewController.CellSectionModel]>
        let loading: Driver<Bool>
    }
}
複製代碼

這裏可能會有人疑問爲何會保存頁面的數據呢,咱們的數據不是直接經過網絡請求生成一個序列綁定到TableView了嗎?由於在某些業務場景下咱們須要保存它,好比在網絡請求錯誤的時候,我但願頁面還會繼續顯示以前有數據的狀態,這時候咱們就能夠在網絡請求錯誤的序列中塞入咱們以前保存的數據,這樣頁面仍是顯示原樣,還有你注意沒有這個屬性是private的。 ActivityIndicator:能夠監聽網絡請求的狀態從而改變loading的狀態,具體實如今下面代碼中已經貼出。

createNewGroup :當點擊頁面上的新建分組按鈕會發送一個序列做爲ViewModel輸入,經過flatMapLatest轉換操做進入到下一頁完成新建分組的操做,並將結果以序列的形式傳回來。這裏的saveData是一個PublishSubject類型,可接收也可發送序列,由於Driver只能接收而不能發送。若是成功就去刷新頁面。

viewDidLoad:當ViewController調用viewDidLoad的方法的時候會發送一個序列做爲ViewModel輸入,經過transform轉化dataSource輸出去更新TableView。

cellDeleteButtonTap和cellRenameButtonTap: 點擊cell中的按鈕,會發出一個序列做爲ViewModel輸入,而後執行相應的業務代碼,最後產生輸出。

dataSource:TableView數據源序列,發生改變會去刷新TableView。

loading:控制頁面loading狀態的序列

ActivityIndicator核心代碼

public class ActivityIndicator: SharedSequenceConvertibleType {
    fileprivate func trackActivityOfObservable<O: ObservableConvertibleType>(_ source: O) -> Observable<O.E> {
        return Observable.using({ () -> ActivityToken<O.E> in
            self.increment()
            return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
        }) { activity in
            return activity.asObservable()
        }
    }
    private func increment() {
        lock.lock()
        value += 1
        subject.onNext(value)
        lock.unlock()
    }
    private func decrement() {
        lock.lock()
        value -= 1
        subject.onNext(value)
        lock.unlock()
    }
}
複製代碼

ViewController中的核心代碼

import UIKit
class MenuSubGroupViewController: UIViewController {
    private let cellDeleteButtonTap = PublishSubject<IndexPath>()  // 刪除分組序列,cell中刪除按鈕點擊時調用onNext方法發送序列
    private let cellRenameButtonTap = PublishSubject<IndexPath>() // 分組重命名序列,cell中重命名按鈕點擊時調用onNext方法發送序列

    // 初始化ViewModel的輸入序列並進行ViewModel的輸出序列綁定到View
    func bindViewModel() {
        let viewDidLoad = Driver<Void>.just(())
        let input = MenuSubGroupViewModel.Input(createNewGroup: createGroupButton.rx.tap.asDriver(),
                                                viewDidLoad: viewDidLoad,
                                                cellDeleteButtonTap: cellDeleteButtonTap.asDriverOnErrorJustComplete(),
                                                cellRenameButtonTap: cellRenameButtonTap.asDriverOnErrorJustComplete())
        
        let output = viewModel.transform(input: input)
        output.loading..
        output.dataSource
            .drive(tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    }
  
    private lazy var dataSource: RxTableViewSectionedReloadDataSource<CellSectionModel> = {
        return RxTableViewSectionedReloadDataSource<CellSectionModel>(configureCell: { [weak self](_, tableView, indexPath, item) -> UITableViewCell in
            let cell: LabelButtonCell = tableView.dequeueReusableCell(LabelButtonCell.self)
            ...
            cell.rightButton1.rx.tap
                .subscribe(onNext: { [weak self] (_) in
                    self?.cellDeleteButtonTap.onNext(indexPath)
                })
                .disposed(by: cell.disposeBag)
            cell.rightButton2.rx.tap...
            return cell
        })
    }()
}
複製代碼

在這裏RxDataSources的使用方法我就再也不詳細敘述了,因此說咱們主要關注bindViewModel的方法,裏面定義了頁面的各類輸入,並經過transform方法等獲得輸出的序列,再對TableView的數據源進行綁定。RxCocoa爲咱們提供了不少系統基礎控件的Rx調用,能夠很方便的進行數據綁定。

Navigator中的核心代碼

class MenuSubGroupNavigator: BaseNavigator {
    func toMenuEditGroupVC(menuUid: String, dishGroupsInfo: DishGroupInfo? = nil) -> MenuEditGroupViewController {
        let navigator = MenuEditGroupNavigator(navigationController: navigationController)
        let viewModel = MenuEditGroupViewModel(navigator: navigator)
        let vc = MenuEditGroupViewController()
        vc.viewModel = viewModel
        navigationController?.pushViewController(vc, animated: true)
        return vc
    }
}
複製代碼

總結

  1. 要搭建一個上述的MVVM項目,RxSwift,RxDataSources,Moya是必不可少的,而且你要會用RxDataSource建立UITableView數據源,對RxSwift要有必定的瞭解。
  2. 在項目中對cell中的點擊事件的處理方式是在ViewController裏建立一個PublishSubject的序列,而後在事件回調或監聽處主動調用onNext方法。
  3. 對於頁面loading,無數據,無網等狀態能夠分別封裝ViewController的Rx屬性,而後經過ActivityIndicator能夠監聽網絡請求的狀態,發送序列從而改變頁面狀態。
  4. 上述的MVVM項目的不少操做都是經過序列來完成的,發生錯誤時可能很差定位。

源碼地址,不過你們能夠參考下GitHub上的CleanArchitectureRxSwift

本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。@xqqlv

本站公眾號
   歡迎關注本站公眾號,獲取更多信息