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


  • 前言

之前写了一篇如何利用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,有时间我会更新这两种用法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容