之前對MVVM的理解和運用以爲很淺薄,在項目中用處只是對ViewController減負git
因此是時候在項目中使用真正的MVVM了(整理出套路代碼),介於項目中已經引入了RxSwift,因此就用它來實現了,在學習本文前可能會要求讀者對RxSwift有必定的瞭解和使用。github
上圖是項目中的一個模塊,使用MVVM架構後的文件結構,Model被我集中的定義在一個公共的文件夾裏了,接下來我會詳細介紹。swift
查閱了許多資料,不一樣人對ViewModel的實現有不少種,我這裏總結了一下多數人也是我比較贊同的一種實現方法 網絡
class ViewModel {
// 輸入轉化輸出,這裏是真正的業務邏輯代碼了
func transform(input: Input) -> Output {
}
}
extension ViewModel {
// 輸入,類型是Driver,由於跟UI控件有關
struct Input {
}
// 輸出,類型也是Driver
struct Output {
}
}
複製代碼
對於Model,它主要是定義一些數據模型,固然你也能夠封裝一些數據轉換等公共的業務方法。架構
ViewController的主要做用是管理視圖的生命週期,綁定數據和View的關係,數據綁定的實現主要是經過RxDataSources+RxSwift來實現的,因此說你的項目中要引入這兩個庫。RxCocoa給UI框架提供了Rx支持,讓咱們可以使用按鈕點擊序列,這樣咱們就能夠給ViewModel提供輸入了,而RxDataSources可以幫助你簡化書寫 TabelView或 CollectionView的數據源這一過程,而且提供了經過序列更新TableView的方法,這時候咱們只要把ViewModel的數據輸出序列綁定到TableView的數據源序列就能夠了。框架
Navigator是從ViewController剝離出來用來控制視圖跳轉學習
下圖是上述目錄結構中一個頁面 測試
先分析下界面上的輸入和輸出ui
輸入:進入頁面時的請求,重命名按鈕點擊,刪除按鈕點擊,新建分組按鈕點擊spa
輸出:TableView數據源,頁面Loading狀態
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狀態的序列
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()
}
}
複製代碼
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調用,能夠很方便的進行數據綁定。
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
}
}
複製代碼
源碼地址,不過你們能夠參考下GitHub上的CleanArchitectureRxSwift。
本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。@xqqlv