返回 导航

Swift

hangge.com

Swift - RxSwift的使用详解68(监听滚动条滚动到底部的行为:reachedBottom)

作者:hangge | 2018-06-18 08:10
    有时我们需要监测滚动条是否滚动到底部,如果滚到底的话则自动执行相应的操作。比如:表格一开始只加载一部分数据并显示,当滚动显示到最后一行时,又会自动加载更多的数据。下面通过样例演示如何实现这个功能。

一、扩展 UIScrollView

(1)为保证通用性,我们首先对 UIScrollView 进行 Rx 扩展(UIScrollView+Rx.swift),增加一个 reachedBottom 的方法。该方法返回一个可观察序列,该序列不断监听滚动条的位置变化,每当视图滚到底或者超出时便会发出事件。
(2)由于是对 UIScrollView 进行扩展,所以该方法对 UIScrollView 的子类(如:UITableViewUICollectionView)也是有效的。
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)

回到顶部