之前对MVVM的理解和运用以为很浅薄,在项目中用处只是对ViewController减负git
因此是时候在项目中使用真正的MVVM了(整理出套路代码),介于项目中已经引入了RxSwift,因此就用它来实现了,在学习本文前可能会要求读者对RxSwift有必定的了解和使用。github
上图是项目中的一个模块,使用MVVM架构后的文件结构,Model被我集中的定义在一个公共的文件夹里了,接下来我会详细介绍。swift
查阅了许多资料,不一样人对ViewModel的实现有不少种,我这里总结了一下多数人也是我比较赞同的一种实现方法 markdown
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剥离出来用来控制视图跳转框架
下图是上述目录结构中一个页面 oop
先分析下界面上的输入和输出学习
输入:进入页面时的请求,重命名按钮点击,删除按钮点击,新建分组按钮点击测试
输出: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