-
前言
之前写了一篇如何利用rxswift实现tableview的文章,那时候刚接触rxswift,对响应式编程和mvvm的理解还不是很透彻,直接扒了一篇外网的文章就翻译了。现在看来,很不适合,其中的内容实用性也很差。今天更这篇利用rxswift实现上下拉刷新的文章,也会谈到tableview的问题。
在rxswift当中更新UI而非业务相关的网络请求,经常会放在viewModel中实现。
viewModel的网络请求后,控制器需要实现回调。如果自己使用通知或者闭包实现回调也是可以的。但是使用rxswift可以减少我们的工作量,也比较优雅(比较优雅这句是我瞎编的 - -#)。
为了以后添加功能方便,以及统一性。所有viewmodel都继承自baseViewModel。
baseViewModel中可以添加一个与刷新有关的变量。
class BaseViewModel: NSObject {
let disposeBag = DisposeBag()
var refreshStatus = Variable.init(RefreshStatus.InvalidData)
}
其中disposeBag是rxswift用来释放资源的一个类。建议所有自定义的基础类都添加这个变量(这里我写的是一个常量,如果你不太确定是否会经常用到它,可以写个lazy var形式的变量声明)。
refreshStatus
是一个variable类型。variable类型是rxswift当中特有的一个类型。它是一个泛型,它的.value属性指向的就是它的实际参数类型。比如在我例子中,variable的实际参数类型是RefreshStatus,它是一个枚举类型。
enum RefreshStatus: Int {
case DropDownSuccess // 下拉成功
case PullSuccessHasMoreData // 上拉,还有更多数据
case PullSuccessNoMoreData // 上拉,没有更多数据
case InvalidData // 无效的数据
}
variable类型的特点在于,只要改变value的值,就会发射改变后的数据。如果你对rxswift不太了解,你只需要知道variable的这个特性就行了。
这就代表着,只要你在viewModel里面的回调方法里改变refreshStatus的值,它就会发射对应的数据。这样在你的控制器中监听数据的变化,就可以响应刷新了。
我在baseViewModel中实现了三个方法,用来处理网络请求回调的普遍操作。
/**
重写刷新方法,发射刷新信号
*/
override func updateData<List>(inout source: [List], list: [List], pullRefresh: Bool) {
super.updateData(&source, list: list, pullRefresh: pullRefresh)
// 刷新处理
if pullRefresh { // 上拉刷新处理
self.refreshStatus.value = self.pageModel.hasNext ? .PullSuccessHasMoreData : .PullSuccessNoMoreData
} else { // 下拉刷新处理
self.refreshStatus.value = .DropDownSuccess
}
}
这是我用来处理分页请求回调成功的方法。其中pullRefresh的布尔值用来判断你是上拉还是下拉。true为上拉,false为下拉。
pageModel用来处理分页。其中的hasNext属性用来判断是否还有下一页。
这样,请求成功后,根据你上下拉的不同,发射不同的信号,你也可以用来做不同的处理。
网络请求失败和出错都会统一调用另外一个方法:
func revertCurrentPageAndRefreshStatus() {
// 修改刷新view的状态
self.refreshStatus.value = .InvalidData
// 还原请求页
self.pageModel.currentPage = self.pageModel.currentPage > 1 ? self.pageModel.currentPage - 1 : 1
}
在请求失败后,把刷新状态置为此次无效。把请求分页还原到当前请求的分页。
这样,在viewModel中的处理我们已经处理好了,下一步就是到控制器中处理回调。
可以声明一个TableController,自带一个tableview控件。为它设置好mj_header和mj_footer。这里是对MJRefresh三房库的调用。如果你不会使用可以直接到Github上查看原作者给出的用例。
在你的控制器中,可以实现一个方法用来做刷新状态的处理。
/**
设置刷新状态
*/
func setUpRefreshStatus() {
tmpViewModel?.refreshStatus.asObservable().bindNext { [unowned self] (status) in
switch status {
case .InvalidData:
self.tableView.endRefreshing()
return
case .DropDownSuccess:
self.tableView.footerResetNoMoreData()
self.tableView.footerEndRefreshing()
case .PullSuccessHasMoreData:
self.tableView.footerEndRefreshing()
case .PullSuccessNoMoreData:
self.tableView.footerEndRefreshWithNoMoreData()
}
self.tableView.headerEndRefreshing()
}.addDisposableTo(disposeBag)
}
其中tmpViewModel写成一个可选类型,代表着每个控制器绑定的viewModel。为什么这里没有写viewModel的实际类型,是因为每个控制器绑定的ViewModel可能是不同的类型,这里是针对baseViewModel的处理。用了?可选类型是因为也许你的控制器并没有一个viewModel。
后面的绑定是rxswift的用法,最后的bindnext闭包是当viewModel的refreshStatus改变value后,所做的回调。
记得最后要添加disposeBag做资源的释放。这里的disposeBag是控制器的属性,并不是viewModel的。
记得在控制器加载后,做下面的操作:
if let vm = self.valueForKey(“viewModel”) as? BaseViewModel {
tmpViewModel = vm // 利用kvc设置tmpViewModel,这样就不需要在每个子类设置了
}
这里是用kvc动态查询你的控制器是否有viewModel属性。记得做查询不到的操作,不然程序会crash。
override func valueForUndefinedKey(key: String) -> AnyObject? {
if key == "viewModel" {
return nil
}
return super.valueForUndefinedKey(key)
}
这样针对整个刷新的操作就结束了。怎么样,也不是很难吧。😁
上面所做的kvc操作是为了你能够有一个基础类设置上下拉刷新,这样你自定义的其他子类就不需要做其他任何操作,就可以完成上下拉刷新的UI改变。
下面谈谈tableview刷新的问题。
tableview的刷新也用了同样的原理。在viewModel中声明一个variable类型的变量。
它的参数类型,是你实际请求后的数据类型。
var dataSource = Variable.init([CoursesModel]())
如果你的页面是一个纯列表页面,每个cell长得都一样,你可以用这样的方法,variable的参数类型是一个数组。
如果你的页面每个cell都不一样,你可以直接variable的变量是一个model。
var trainInfoData = Variable.init(TrainInfoModel())
在网络请求成功的回调里,改变变量的值
self.trainInfoData.value = model
上面的model是你请求回来的数据。
这样,在控制器里做对variable的观察。和上下拉刷新是一样的。
这里的区别在于,如果你是用列表形式的,可以直接用rxswift提供的绑定方法:
viewModel.dataSource.asObservable().bindTo(tableView.rx_itemsWithCellIdentifier(reuseIdentifier, cellType: ClassesCell.self)) {
row, model, cell in
cell.model = model
}.addDisposableTo(disposeBag)
它默认你的tableview只有一个分组,你不需要实现tableview的数据源方法。
如果的页面不是列表形式的,你可以只做监听,自己实现tableview的数据源方法(RxDataSource提供了另外的方法,你如果想用使用也可以)。
viewModel.dataSource.asObservable().bindNext { [unowned self] model in self.tableView.reloadData() }.addDisposableTo(disposeBag)
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier0, forIndexPath: indexPath) as! MineFirstCell
cell.model = viewModel.dataSource.value
cell.setIndexPath(indexPath.section)
return cell
}
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier1, forIndexPath: indexPath) as! TrainInfoCell
cell.model = viewModel.dataSource.value
cell.setIndexPath(indexPath.section)
return cell
}
如果是自己实现了数据源方法,需要在监听回调里刷新tableview,然后传回来的model就是请求后拿到的model。
如果是列表的方法,rxswift帮我们做了刷新列表。不需要我们再手动刷新了。
这就是rxswift的tableview的用法。
其实使用这个用法,我们还可以为每一个控制器添加第一次进入请求时的遮罩view,以及请求失败或者没有数据的成errorview,有时间我会更新这两种用法。