今天在微博上看到了一个 MVVM + RxSwift 的项目,RxTodo (opens new window)。看了一下 README ,感觉是个不错的项目,阅读了一下源码,值得学习。
您可以在本文中了解到以下内容:
先从 README (opens new window) 开始。
看了 Philisophy 部分,和笔者观点不谋而合。
笔者倾向于认为更改数据属于业务逻辑, ViewController 只应该放视图逻辑。二者关系如图:
换句话说就是 ViewController 不需要知道 ViewModel 到底做了什么,只需要满足 ViewModel 具体的输入逻辑,比如:
self.loginButton.rx_tap
.bindTo(viewModel.loginButton)
这段代码就是表明了 ViewModel 中的登录事件是通过 loginButton
点击产生的。
这是一个非常好的观点,不暴露 Model ,说明依赖关系是这样的:
事实上我写的时候是暴露的(尴尬):
不过我在设置 View 时是不依赖 Model 的,比如
view.set(title: title)
。
我最先看到的是 BaseViewController
和 BaseTableViewCell
,分别提供了 func setupConstraints()
和 func initialize()
抽象方法,很好,我很喜欢,这样就不需要在重复写很多 init
方法。
之前在 GMTC 有位 ThoughtWorks 的朋友来问我一些实践的问题,其中一个就是 viewDidLoad
中写了很多声明逻辑关系的代码。我想这个是有两种比较好的解决方案的:
类似上面这样写一个 func setupViewModel()
之类的方法,将代码分离开,当然具体如何写这样的方法还是要看具体的逻辑。
写多个 MARK ,每个 MARK 标记一下这里的逻辑,用这样的方式将代码逻辑变得更工整写。
当然了,记得我们有
// MARK: -
和// MARK:
帮助我们更好的对代码进行分级。
虽然我们并没有解决代码臃肿的问题,然而这也没什么关系,毕竟这些代码总是要有的,放哪里都是放。阅读起来更方便一些就好了~此外对于臃肿问题,还有一种比较好的解决方案,笔者将在后面的文章中进行介绍。
以 TaskListViewModel
为例解读如何写一个 ViewModel
:
// 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
:有一个新的编辑任务这并不难理解,如果我们不关心内部实现的话,现在我们就只需要指出对应的输入行为,比如添加行为:
self.addBarButtonItem.rx_tap
.bindTo(viewModel.addButtonDidTap)
.addDisposableTo(self.disposeBag)
接下来就只需要指出对应的展示结果,比如所有要展示的任务:
viewModel.sections
.drive(self.tableView.rx_itemsWithDataSource(self.dataSource))
.addDisposableTo(self.disposeBag)
至于 ViewModel 的内部实现也不难理解:
self.itemDeleted
.subscribeNext { indexPath in
let task = tasks.value[indexPath.row]
Task.didDelete.onNext(task)
}
.addDisposableTo(self.disposeBag)
有删除 item 时,就删除 ViewModel 内部的一个 task ,同时告诉 Task
我删除了一个 task 。
Task.didDelete
.subscribeNext { task in
if let index = self.tasks.value.indexOf(task) {
self.tasks.value.removeAtIndex(index)
}
}
.addDisposableTo(self.disposeBag)
事实上,上面这些代码基本上都是在表达怎么做,而不是做什么。
当然这个 RxTodo 项目因为加入了 ViewModel ,不方便直接解释,我们换一种形式,对于添加任务这个功能的逻辑就是:
点击添加按钮,进入编辑任务信息界面,然后输入任务信息,输入完毕后,根据任务信息添加到当前任务列表。
上面这段话就是怎么做,也就是所谓的声明。
用代码表示大概是这个样子:
// TaskListViewController
addButton.rx_tap // 点击添加按钮
.subscribeNext(showTaskEdit) // 进入编辑任务信息界面
// TaskEditViewController
ensureButton.rx_tap
.withLatestFrom(taskInfo) // 确认输入完毕
.subscribeNext(Task.add) // 添加到当前任务列表
可以看到,我们可以很清楚的用代码表述业务逻辑。
放到初始化大概会是这个样子:
// TaskEditViewModel
init(input: (
cancelButtonDidTap: Observable<Void>,
doneButtonDidTap: Observable<Void>,
alertLeaveButtonDidTap: Observable<Void>,
alertStayButtonDidTap: Observable<Void>,
memo: Observable<String>
)
) {
// ...
}
有两个好处:
PublishSubject
还可以作为 Observable
,可以被外界(ViewController)当做输出使用,这一点就比较尴尬了粗略的写了一下,如有错误或是不合理还请直接指出来~