今天在微博上看到了一个 MVVM + RxSwift 的项目,RxTodo。看了一下 README ,感觉是个不错的项目,阅读了一下源码,值得学习。

摘要

您可以在本文中了解到以下内容:

  • 清晰合理的 ViewModel
  • 声明式编程

README

先从 README 开始。

看了 Philisophy 部分,和笔者观点不谋而合。

ViewController 不应该更改数据

笔者倾向于认为更改数据属于业务逻辑, ViewController 只应该放视图逻辑。二者关系如图:

ViewController 不需要知道 ViewModel 的具体行为

换句话说就是 ViewController 不需要知道 ViewModel 到底做了什么,只需要满足 ViewModel 具体的输入逻辑,比如:

1
2
self.loginButton.rx_tap
.bindTo(viewModel.loginButton)

这段代码就是表明了 ViewModel 中的登录事件是通过 loginButton 点击产生的。

用 ViewModel 隐藏 Model

这是一个非常好的观点,不暴露 Model ,说明依赖关系是这样的:

事实上我写的时候是暴露的(尴尬):

不过我在设置 View 时是不依赖 Model 的,比如 view.set(title: title)

代码学习

Base

我最先看到的是 BaseViewControllerBaseTableViewCell ,分别提供了 func setupConstraints()func initialize() 抽象方法,很好,我很喜欢,这样就不需要在重复写很多 init 方法。

viewDidLoad 中代码臃肿

之前在 GMTC 有位 ThoughtWorks 的朋友来问我一些实践的问题,其中一个就是 viewDidLoad 中写了很多声明逻辑关系的代码。我想这个是有两种比较好的解决方案的:
类似上面这样写一个 func setupViewModel() 之类的方法,将代码分离开,当然具体如何写这样的方法还是要看具体的逻辑。
写多个 MARK ,每个 MARK 标记一下这里的逻辑,用这样的方式将代码逻辑变得更工整写。

当然了,记得我们有 // MARK: -// MARK: 帮助我们更好的对代码进行分级。

虽然我们并没有解决代码臃肿的问题,然而这也没什么关系,毕竟这些代码总是要有的,放哪里都是放。阅读起来更方便一些就好了~此外对于臃肿问题,还有一种比较好的解决方案,笔者将在后面的文章中进行介绍。

TaskListViewModel

TaskListViewModel 为例解读如何写一个 ViewModel

1
2
3
4
5
6
7
8
// MARK: Input
let addButtonDidTap = PublishSubject<Void>()
let itemDidSelect = PublishSubject<NSIndexPath>()
var itemDeleted = PublishSubject<NSIndexPath>()
// MARK: Output
let navigationBarTitle: Driver<String?>
let sections: Driver<[TaskListSection]>
let presentTaskEditViewModel: Driver<TaskEditViewModel>

作者 devxoul 已经标出来了,定义了三个输入行为:

  • addButtonDidTap :添加行为
  • itemDidSelect : 选择某个 item 行为
  • itemDeleted :删除某个 item 行为

三个输出结果:

  • navigationBarTitle : 导航栏的标题
  • sections :所有要展示的任务
  • presentTaskEditViewModel :有一个新的编辑任务

这并不难理解,如果我们不关心内部实现的话,现在我们就只需要指出对应的输入行为,比如添加行为

1
2
3
self.addBarButtonItem.rx_tap
.bindTo(viewModel.addButtonDidTap)
.addDisposableTo(self.disposeBag)

接下来就只需要指出对应的展示结果,比如所有要展示的任务

1
2
3
viewModel.sections
.drive(self.tableView.rx_itemsWithDataSource(self.dataSource))
.addDisposableTo(self.disposeBag)

至于 ViewModel 的内部实现也不难理解:

1
2
3
4
5
6
self.itemDeleted
.subscribeNext { indexPath in
let task = tasks.value[indexPath.row]
Task.didDelete.onNext(task)
}
.addDisposableTo(self.disposeBag)

有删除 item 时,就删除 ViewModel 内部的一个 task ,同时告诉 Task 我删除了一个 task 。

1
2
3
4
5
6
7
Task.didDelete
.subscribeNext { task in
if let index = self.tasks.value.indexOf(task) {
self.tasks.value.removeAtIndex(index)
}
}
.addDisposableTo(self.disposeBag)

事实上,上面这些代码基本上都是在表达怎么做,而不是做什么。

当然这个 RxTodo 项目因为加入了 ViewModel ,不方便直接解释,我们换一种形式,对于添加任务这个功能的逻辑就是:

点击添加按钮,进入编辑任务信息界面,然后输入任务信息,输入完毕后,根据任务信息添加到当前任务列表。

上面这段话就是怎么做,也就是所谓的声明。

用代码表示大概是这个样子:

1
2
3
4
5
6
7
8
// TaskListViewController
addButton.rx_tap // 点击添加按钮
.subscribeNext(showTaskEdit) // 进入编辑任务信息界面

// TaskEditViewController
ensureButton.rx_tap
.withLatestFrom(taskInfo) // 确认输入完毕
.subscribeNext(Task.add) // 添加到当前任务列表

可以看到,我们可以很清楚的用代码表述业务逻辑。

笔者对于该项目的一些建议

Input 放到初始化

放到初始化大概会是这个样子:

1
2
3
4
5
6
7
8
9
10
11
// TaskEditViewModel
init(input: (
cancelButtonDidTap: Observable<Void>,
doneButtonDidTap: Observable<Void>,
alertLeaveButtonDidTap: Observable<Void>,
alertStayButtonDidTap: Observable<Void>,
memo: Observable<String>
)
) {
// ...
}

有两个好处:

  • 不需要标注哪些是输入,同时避免指出一个输入的行为
  • PublishSubject 还可以作为 Observable ,可以被外界(ViewController)当做输出使用,这一点就比较尴尬了

粗略的写了一下,如有错误或是不合理还请直接指出来~