当前位置:K88软件开发文章中心编程语言JavaScriptJS01 → 文章内容

【RxSwift系列】RxSwift下如何实现基于MJRefresh的上下拉刷新?

减小字体 增大字体 作者:佚名  来源:网上搜集  发布时间:2019-1-4 8:50:57

-->
  • 前言

之前写了一篇如何利用rxswift实现tableview的文章,那时候刚接触rxswift,对响应式编程和mvvm的理解还不是很透彻,直接扒了一篇外网的文章就翻译了。现在看来,很不适合,其中的内容实用性也很差。今天更这篇利用rxswift实现上下拉刷新的文章,也会谈到tableview的问题。


在rxswift当中更新UI而非业务相关的网络请求,经常会放在viewModel中实现。
viewModel的网络请求后,控制器需要实现回调。如果自己使用通知或者闭包实现回调也是可以的。但是使用rxswift可以减少我们的工作量,也比较优雅(比较优雅这句是我瞎编的 – -#)。
为了以后添加功能方便,以及统一性。所有viewmodel都继承自baseViewModel。
baseViewModel中可以添加一个与刷新有关的变量。

1
2
3
4
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseViewModel</span>: <span class="hljs-title">NSObject</span> </span>{
<span class="hljs-keyword">let</span> disposeBag = <span class="hljs-type">DisposeBag</span>()
<span class="hljs-keyword">var</span> refreshStatus = <span class="hljs-type">Variable</span>.<span class="hljs-keyword">init</span>(<span class="hljs-type">RefreshStatus</span>.<span class="hljs-type">InvalidData</span>)
}

其中disposeBag是rxswift用来释放资源的一个类。建议所有自定义的基础类都添加这个变量(这里我写的是一个常量,如果你不太确定是否会经常用到它,可以写个lazy var形式的变量声明)。

1
refreshStatus

是一个variable类型。variable类型是rxswift当中特有的一个类型。它是一个泛型,它的.value属性指向的就是它的实际参数类型。比如在我例子中,variable的实际参数类型是RefreshStatus,它是一个枚举类型。

1
2
3
4
5
6
<span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">RefreshStatus</span>: <span class="hljs-title">Int</span> </span>{
<span class="hljs-keyword">case</span> <span class="hljs-type">DropDownSuccess</span> <span class="hljs-comment">// 下拉成功</span>
<span class="hljs-keyword">case</span> <span class="hljs-type">PullSuccessHasMoreData</span> <span class="hljs-comment">// 上拉,还有更多数据</span>
<span class="hljs-keyword">case</span> <span class="hljs-type">PullSuccessNoMoreData</span> <span class="hljs-comment">// 上拉,没有更多数据</span>
<span class="hljs-keyword">case</span> <span class="hljs-type">InvalidData</span> <span class="hljs-comment">// 无效的数据</span>
}

variable类型的特点在于,只要改变value的值,就会发射改变后的数据。如果你对rxswift不太了解,你只需要知道variable的这个特性就行了。
这就代表着,只要你在viewModel里面的回调方法里改变refreshStatus的值,它就会发射对应的数据。这样在你的控制器中监听数据的变化,就可以响应刷新了。

我在baseViewModel中实现了三个方法,用来处理网络请求回调的普遍操作。

1
2
3
4
5
6
7
8
9
10
11
12
<span class="hljs-comment">/**
 重写刷新方法,发射刷新信号
 */</span>
<span class="hljs-keyword">override</span> <span class="hljs-func"><span class="hljs-keyword">func</span> <span class="hljs-title">updateData</span><span class="hljs-generics"><List></span><span class="hljs-params">(<span class="hljs-keyword">inout</span> source: [List], list: [List], pullRefresh: Bool)</span></span> {
    <span class="hljs-keyword">super</span>.updateData(&source, list: list, pullRefresh: pullRefresh)
    <span class="hljs-comment">// 刷新处理</span>
    <span class="hljs-keyword">if</span> pullRefresh {  <span class="hljs-comment">// 上拉刷新处理</span>
        <span class="hljs-keyword">self</span>.refreshStatus.value = <span class="hljs-keyword">self</span>.pageModel.hasNext ? .<span class="hljs-type">PullSuccessHasMoreData</span> : .<span class="hljs-type">PullSuccessNoMoreData</span>
    } <span class="hljs-keyword">else</span> { <span class="hljs-comment">// 下拉刷新处理</span>
        <span class="hljs-keyword">self</span>.refreshStatus.value = .<span class="hljs-type">DropDownSuccess</span>
    }
}

这是我用来处理分页请求回调成功的方法。其中pullRefresh的布尔值用来判断你是上拉还是下拉。true为上拉,false为下拉。
pageModel用来处理分页。其中的hasNext属性用来判断是否还有下一页。
这样,请求成功后,根据你上下拉的不同,发射不同的信号,你也可以用来做不同的处理。

网络请求失败和出错都会统一调用另外一个方法:

1
2
3
4
5
6
 <span class="hljs-func"><span class="hljs-keyword">func</span> <span class="hljs-title">revertCurrentPageAndRefreshStatus</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// 修改刷新view的状态</span>
    <span class="hljs-keyword">self</span>.refreshStatus.value = .<span class="hljs-type">InvalidData</span>
    <span class="hljs-comment">// 还原请求页</span>
    <span class="hljs-keyword">self</span>.pageModel.currentPage = <span class="hljs-keyword">self</span>.pageModel.currentPage > <span class="hljs-number">1</span> ? <span class="hljs-keyword">self</span>.pageModel.currentPage - <span class="hljs-number">1</span> : <span class="hljs-number">1</span>
}

在请求失败后,把刷新状态置为此次无效。把请求分页还原到当前请求的分页。

这样,在viewModel中的处理我们已经处理好了,下一步就是到控制器中处理回调。
可以声明一个TableController,自带一个tableview控件。为它设置好mj_header和mj_footer。这里是对MJRefresh三房库的调用。如果你不会使用可以直接到Github上查看原作者给出的用例。
在你的控制器中,可以实现一个方法用来做刷新状态的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<span class="hljs-comment">/**
 设置刷新状态
 */</span>
<span class="hljs-func"><span class="hljs-keyword">func</span> <span class="hljs-title">setUpRefreshStatus</span><span class="hljs-params">()</span></span> {
    tmpViewModel?.refreshStatus.asObservable().bindNext { [<span class="hljs-keyword">unowned</span> <span class="hljs-keyword">self</span>] (status) <span class="hljs-keyword">in</span>
        <span class="hljs-keyword">switch</span> status {
        <span class="hljs-keyword">case</span> .<span class="hljs-type">InvalidData</span>:
            <span class="hljs-keyword">self</span>.tableView.endRefreshing()
            <span class="hljs-keyword">return</span>
        <span class="hljs-keyword">case</span> .<span class="hljs-type">DropDownSuccess</span>:
            <span class="hljs-keyword">self</span>.tableView.footerResetNoMoreData()
            <span class="hljs-keyword">self</span>.tableView.footerEndRefreshing()
        <span class="hljs-keyword">case</span> .<span class="hljs-type">PullSuccessHasMoreData</span>:
            <span class="hljs-keyword">self</span>.tableView.footerEndRefreshing()
        <span class="hljs-keyword">case</span> .<span class="hljs-type">PullSuccessNoMoreData</span>:
            <span class="hljs-keyword">self</span>.tableView.footerEndRefreshWithNoMoreData()
        }
        <span class="hljs-keyword">self</span>.tableView.headerEndRefreshing()
        }.addDisposableTo(disposeBag)
}

其中tmpViewModel写成一个可选类型,代表着每个控制器绑定的viewModel。为什么这里没有写viewModel的实际类型,是因为每个控制器绑定的ViewModel可能是不同的类型,这里是针对baseViewModel的处理。用了?可选类型是因为也许你的控制器并没有一个viewModel。
后面的绑定是rxswift的用法,最后的bindnext闭包是当viewModel的refreshStatus改变value后,所做的回调。
记得最后要添加disposeBag做资源的释放。这里的disposeBag是控制器的属性,并不是viewModel的。

记得在控制器加载后,做下面的操作:

1
2
3
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> vm = <span class="hljs-keyword">self</span>.valueForKey(“viewModel”) <span class="hljs-keyword">as</span>? <span class="hljs-type">BaseViewModel</span> {
        tmpViewModel = vm <span class="hljs-comment">// 利用kvc设置tmpViewModel,这样就不需要在每个子类设置了</span>
    }

这里是用kvc动态查询你的控制器是否有viewModel属性。记得做查询不到的操作,不然程序会crash。

1
2
3
4
5
6
<span class="hljs-keyword">override</span> <span class="hljs-func"><span class="hljs-keyword">func</span> <span class="hljs-title">valueForUndefinedKey</span><span class="hljs-params">(key: String)</span></span> -> <span class="hljs-type">AnyObject</span>? {
    <span class="hljs-keyword">if</span> key == <span class="hljs-string">"viewModel"</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    }
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.valueForUndefinedKey(key)
}

这样针对整个刷新的操作就结束了。怎么样,也不是很难吧。😁
上面所做的kvc操作是为了你能够有一个基础类设置上下拉刷新,这样你自定义的其他子类就不需要做其他任何操作,就可以完成上下拉刷新的UI改变。
下面谈谈tableview刷新的问题。


tableview的刷新也用了同样的原理。在viewModel中声明一个variable类型的变量。
它的参数类型,是你实际请求后的数据类型。

1
var dataSource = Variable.init([CoursesModel]())

如果你的页面是一个纯列表页面,每个cell长得都一样,你可以用这样的方法,variable的参数类型是一个数组。
如果你的页面每个cell都不一样,你可以直接variable的变量是一个model。

1
var trainInfoData = Variable.init(TrainInfoModel())

网络请求成功的回调里,改变变量的值

1
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提供了另外的方法,你如果想用使用也可以)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
viewModel.dataSource.asObservable().bindNext { [<span class="hljs-keyword">unowned</span> <span class="hljs-keyword">self</span>] model <span class="hljs-keyword">in</span> <span class="hljs-keyword">self</span>.tableView.reloadData() }.addDisposableTo(disposeBag)

<span class="hljs-func"><span class="hljs-keyword">func</span> <span class="hljs-title">numberOfSectionsInTableView</span><span class="hljs-params">(tableView: UITableView)</span></span> -> <span class="hljs-type">Int</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-number">2</span>
}

<span class="hljs-func"><span class="hljs-keyword">func</span> <span class="hljs-title">tableView</span><span class="hljs-params">(tableView: UITableView, numberOfRowsInSection section: Int)</span></span> -> <span class="hljs-type">Int</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
}

<span class="hljs-func"><span class="hljs-keyword">func</span> <span class="hljs-title">tableView</span><span class="hljs-params">(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)</span></span> -> <span class="hljs-type">UITableViewCell</span> {

    <span class="hljs-keyword">if</span> indexPath.section == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">let</span> cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier0, forIndexPath: indexPath) <span class="hljs-keyword">as</span>! <span class="hljs-type">MineFirstCell</span>
        cell.model = viewModel.dataSource.value
        cell.setIndexPath(indexPath.section)
        <span class="hljs-keyword">return</span> cell
    }

    <span class="hljs-keyword">let</span> cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier1, forIndexPath: indexPath) <span class="hljs-keyword">as</span>! <span class="hljs-type">TrainInfoCell</span>
    cell.model = viewModel.dataSource.value
    cell.setIndexPath(indexPath.section)
    <span class="hljs-keyword">return</span> cell

}

如果是自己实现了数据源方法,需要在监听回调里刷新tableview,然后传回来的model就是请求后拿到的model。
如果是列表的方法,rxswift帮我们做了刷新列表。不需要我们再手动刷新了。
这就是rxswift的tableview的用法。
其实使用这个用法,我们还可以为每一个控制器添加第一次进入请求时的遮罩view,以及请求失败或者没有数据的成errorview,有时间我会更新这两种用法。

文/马克叔_Marco(简书作者)
原文链接:http://www.jianshu.com/p/fff7ef50dbb1
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

【RxSwift系列】RxSwift下如何实现基于MJRefresh的上下拉刷新?