Swift - RxSwift的使用详解68(监听滚动条滚动到底部的行为:reachedBottom)
作者:hangge | 2018-06-18 08:10
有时我们需要监测滚动条是否滚动到底部,如果滚到底的话则自动执行相应的操作。比如:表格一开始只加载一部分数据并显示,当滚动显示到最后一行时,又会自动加载更多的数据。下面通过样例演示如何实现这个功能。
一、扩展 UIScrollView
(1)为保证通用性,我们首先对 UIScrollView 进行 Rx 扩展(UIScrollView+Rx.swift),增加一个 reachedBottom 的方法。该方法返回一个可观察序列,该序列不断监听滚动条的位置变化,每当视图滚到底或者超出时便会发出事件。
(2)由于是对 UIScrollView 进行扩展,所以该方法对 UIScrollView 的子类(如:UITableView、UICollectionView)也是有效的。
import UIKit
import RxSwift
import RxCocoa
extension Reactive where Base: UIScrollView {
//视图滚到底部检测序列
var reachedBottom: Signal<()> {
return contentOffset.asDriver()
.flatMap { [weak base] contentOffset -> Signal<()> in
guard let scrollView = base else {
return Signal.empty()
}
//可视区域高度
let visibleHeight = scrollView.frame.height - scrollView.contentInset.top
- scrollView.contentInset.bottom
//滚动条最大位置
let threshold = max(0.0, scrollView.contentSize.height - visibleHeight)
//如果当前位置超出最大位置则发出一个事件
let y = contentOffset.y + scrollView.contentInset.top
return y > threshold ? Signal.just(()) : Signal.empty()
}
}
}
二、使用样例
1,效果图
(1)页面初始化完毕后,自动生成 20 条随机数据显示到表格中。
(2)当表格滑动到底部时,继续生成 20 条新数据并拼接到原数据的下方显示。
(3)当表格再次滑动到底部时,重复上面的动作。

2,样例代码
注意:这里我使用了 flatMapFirst 操作符防止在数据没有返回的时候,多次上拉造成重复请求。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
//表格数据序列
let tableData = BehaviorRelay<[String]>(value: [])
var tableView:UITableView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
//创建表格视图
self.tableView = UITableView(frame: self.view.frame, style:.plain)
//创建一个重用的单元格
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
//单元格数据的绑定
self.tableData.asDriver()
.drive(tableView.rx.items) { (tableView, row, element) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(row+1)、\(element)"
return cell
}
.disposed(by: disposeBag)
//上拉加载数据
self.tableView.rx.reachedBottom.asObservable()
.startWith(()) //初始化完毕时会自动加载一次数据
.flatMapFirst(getRandomResult) //防止重复加载
.subscribe(onNext: { [weak self] items in
if let tableData = self?.tableData {
tableData.accept(tableData.value + items )
}
})
.disposed(by: disposeBag)
}
//获取随机数据
func getRandomResult() -> Driver<[String]> {
print("正在请求数据......")
//随机生成20条数据
let items = Array(0..<20).map{ _ in "随机条目\(arc4random())"}
let observable = Observable.just(items)
return observable
.delay(2, scheduler: MainScheduler.instance)
.asDriver(onErrorDriveWith: Driver.empty())
}
}
附:功能改进
上面的样例在加载数据的时候界面没有任何提示,用户体验不好。下面对此进行优化。
1,效果图
(1)如果表格正在加载数据,表格尾部会有一个活动指示器。
(2)当请求结束后活动指示器自动消失。

2,样例代码
主要是加了个名为 isLoading 的序列,表示当前是否正在加载数据。该序列用来控制是否需要发送请求,以及活动指示器是否显示。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
//表格数据序列
let tableData = BehaviorRelay<[String]>(value: [])
//当前是否正在加载序列
var isLoading = BehaviorRelay<Bool>(value: false)
var tableView:UITableView!
//表格底部用来提示数据加载的视图
var loadMoreView:UIView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
//创建表格视图
self.tableView = UITableView(frame: self.view.frame, style:.plain)
//创建一个重用的单元格
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
//初始化表格尾部的上拉刷新视图
self.setupInfiniteScrollingView()
//单元格数据的绑定
self.tableData.asDriver()
.drive(tableView.rx.items) { (tableView, row, element) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(row+1)、\(element)"
return cell
}
.disposed(by: disposeBag)
//表格尾部的上拉加载视图显示绑定
self.isLoading.asDriver()
.drive(onNext: {
if $0 {
self.tableView.tableFooterView = self.loadMoreView
}else{
self.tableView.tableFooterView = nil
}
})
.disposed(by: disposeBag)
//上拉加载数据
self.tableView.rx.reachedBottom.asObservable()
.startWith(()) //初始化完毕时会自动加载一次数据
.flatMapFirst(getRandomResult) //防止重复加载
.subscribe(onNext: { [weak self] items in
if let tableData = self?.tableData {
tableData.accept(tableData.value + items )
}
self?.isLoading.accept(false)
})
.disposed(by: disposeBag)
}
//获取随机数据
func getRandomResult() -> Driver<[String]> {
print("正在请求数据......")
self.isLoading.accept(true)
//随机生成20条数据
let items = Array(0..<20).map{ _ in "随机条目\(arc4random())"}
let observable = Observable.just(items)
return observable
.delay(3, scheduler: MainScheduler.instance)
.asDriver(onErrorDriveWith: Driver.empty())
}
//上拉加载视图
private func setupInfiniteScrollingView() {
self.loadMoreView = UIView(frame: CGRect(x:0, y:self.tableView.contentSize.height,
width:self.tableView.bounds.size.width, height:40))
self.loadMoreView!.autoresizingMask = .flexibleWidth
//添加中间的环形进度条
let activityViewIndicator = UIActivityIndicatorView(activityIndicatorStyle: .white)
activityViewIndicator.color = .darkGray
let indicatorX = self.loadMoreView!.frame.size.width/2
- activityViewIndicator.frame.width/2
let indicatorY = self.loadMoreView!.frame.size.height/2
- activityViewIndicator.frame.height/2
activityViewIndicator.frame = CGRect(x:indicatorX, y:indicatorY,
width:activityViewIndicator.frame.width,
height:activityViewIndicator.frame.height)
activityViewIndicator.startAnimating()
self.loadMoreView!.addSubview(activityViewIndicator)
}
}
全部评论(0)