RxSwift + MJRefresh 自動管理重新整理狀態

一個絕望的氣純發表於2018-01-04

  參照 RxSwift + MJRefresh 打造自動處理重新整理控制元件狀態 這位大佬的文章,有點抄襲的味道。

列舉

  首先定義一個有關重新整理狀態的列舉型別:

/// 可按照自己的需求新增,由於我沒有用到 mj_footer.beginRefreshing(),
/// 所以沒有定義相關的列舉。
enum RefreshStatus {
    case none
    case beingHeaderRefresh
    case endHeaderRefresh
    case endFooterRefresh
    // 這個列舉由於在我專案中經常用到,所以我定一個關聯值的列舉。
    // 專案中需要:
    //  - 資料為空的時候隱藏 `mj_footer`,否則顯示;
    //  - 然後判斷沒有更多資料就呼叫 `endRefreshingWithNoMoreData()`
    //    否則 `endRefreshing()`
    case footerStatus(isHidden: Bool, isNoMoreData: Bool)
}
複製程式碼

  無需過多糾結,後面會演示列舉如何使用。

BehaviorSubject

  接下來要介紹一個跟 RxSwift 有關的一個型別 BehaviorSubject,我們會在文章用到它。

  BehaviorSubject 向所有訂閱者釋出事件,並向新的訂閱者提供最近(或最初)的值。

  怎麼理解?來看看程式碼:

func addObserver(_ id: String) -> Disposable {
    return subscribe { print("Subscription:", id, "Event:", $0) }
}
    
let disposeBag = DisposeBag()
let subject = BehaviorSubject(value: "?")

subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("?")
subject.onNext("?")

subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("?️")
subject.onNext("?️")

/**
Subscription: 1 Event: next(?)
Subscription: 1 Event: next(?)
Subscription: 1 Event: next(?)
Subscription: 2 Event: next(?)
Subscription: 1 Event: next(?️)
Subscription: 2 Event: next(?️)
Subscription: 1 Event: next(?️)
Subscription: 2 Event: next(?️)
Subscription: 1 Event: next(?)
Subscription: 2 Event: next(?)
Subscription: 1 Event: next(?)
Subscription: 2 Event: next(?)
*/
複製程式碼

  總結一下:BehaviorSubject 從上至下接收它發出的最新(原始值也屬於發出的事件元素)值。並向新的訂閱者提供最新值,所以這裡我們 訂閱2號 會接收到前面發出的最新元素,稍後才是 訂閱2號 自己發出的元素事件。

  這裡請允許我搬布官方的圖例來說明一下:

BehaviorSubject

  如你所見,紫燈的時候訂閱,會接收到紫燈以及之後的所有元素。綠燈(可以理解為我們的 訂閱2號,也就是傳說中的新訂閱者)的時候訂閱,接收到綠燈以及之後的所有元素。

  圖片來源:reactivex.io/documentati…,對 RxSwift 感興趣的同學也可以看下我最近釋出的 RxSwift 系列文章。

協議

  接下來我們要用到協議,用來封裝和重新整理狀態有關的東西。對 Swift 協議還不是太明白的可以繼續看上面那位大佬寫的文章:

iOS - Swift 面向協議程式設計(一)

iOS - Swift 面向協議程式設計(二)

開始

  好了,下面我們就用上面學到的所有知識來寫一個自動管理重新整理狀態案例。

  假設有這樣一個需求:

// ViewController.swift

// 兩個閉包的引數預設為 nil,根據引數自動建立 mj_header 或 mj_footer,
// 不傳引數則不建立。自動管理重新整理狀態。
viewModel.refreshStatusBind(to: tableView, {
    // 處理頭部重新整理。
}) {
    // 處理尾部重新整理。
}.disposed(by: bag)
複製程式碼

  如何做到這一點?看到方法是從 viewModel 裡面調出來的,那我們就去 viewModel 裡面看一看究竟。

class ViewModel: Refreshable {

    lazy var list = Variable<[MnlDakaCommentModel]>([])
    let refreshStatus = BehaviorSubject(value: RefreshStatus.none)
    let reload = PublishSubject<Bool>()
    let bag = DisposeBag()
    
    init() {
        reload.subscribe(onNext: { [weak self] (isDown) in
            guard let `self` = self else {
                return
            }
            // 傳送請求
            MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in 
                let list = result.value?["list"].arrayObject
                let models = decode([MnlDakaCommentModel].self, from: list) ?? []
            }
        }
    }
}
複製程式碼

  好了,為了簡潔刪除了部分程式碼,但該有的還是有,而且 ViewModel 裡面我確實沒有建立 refreshStatusBind(to:) 方法。

  這就奇怪了,究竟方法從何而來?答案在於協議。注意我們開始簽了一個 Refreshable 的協議,refreshStatusBind(to:) 是在裡面定義的。那我們就去看看,這個方法究竟是什麼?為什麼傳幾個引數進去就能自動建立重新整理控制元件並管理其狀態了呢?

Refreshable

  首先,我們定義了一個 Refreshable 協議:

protocol Refreshable {
    var refreshStatus: BehaviorSubject<RefreshStatus> { get }
}
複製程式碼

  這裡你就知道了吧?任何實現 Refreshable 必須實現 refreshStatus 屬性,如果你足夠眼尖應該看到,上面的 ViewModel 同樣定義一個型別一樣 refreshStatus 屬性,為的就是實現協議中規定的屬性。

  好了,有個這個屬性之後,我們就可以愉快的管理重新整理狀態了,比如想讓它結束重新整理,我們可以拿到 refreshStatus 屬性,比如在 ViewModel 裡,我們可以這樣:

// 傳送請求
MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in 
    let list = result.value?["list"].arrayObject
    let models = decode([MnlDakaCommentModel].self, from: list) ?? []
    // 請求完成需要結束重新整理:
    // refreshStatus.onNext(.endFooterRefresh)
    // 或者判斷沒有更多資料時:
    // refreshStatus.onNext(isHidden: false, isNoMoreData: true)
}
複製程式碼

  這時你肯定問了,憑什麼我這樣傳送訊息就可以管理重新整理狀態了?你逗我呢?之前講 BehaviorSubject 的時候不是有講到訂閱 (subscribe) 嗎?既然這裡傳送訊息了,肯定會在接收到發出的元素的時候做了什麼處理吧?

  問得好!問得非常好!問得太————好了。好吧,我老實交代,就來說下接收到元素時我都做了什麼?

extension Refreshable {
    
    func refreshStatusBind(to scrollView: UIScrollView, _ header: (() -> Void)? = nil, _ footer: (() -> Void)? = nil) -> Disposable {
        
        if header != nil {
            scrollView.mj_header = MJRefreshNormalHeader {
                // 處理頭部方法時結束尾部重新整理。
                scrollView.mj_footer?.endRefreshing()
                header?()
            }
        }
        if footer != nil {
            scrollView.mj_footer = MJRefreshAutoNormalFooter {
                // 處理尾部方法時結束頭部重新整理。
                scrollView.mj_header?.endRefreshing()
                footer?()
            }
        }
        
        return refreshStatus.subscribe(onNext: { (status) in
            switch status {
            case .none:
                // 未發生任何狀態事件時隱藏尾部。
                scrollView.mj_footer?.isHidden = true
            case .beginHeaderRefresh:
                scrollView.mj_header?.beginRefreshing()
            case .endHeaderRefresh:
                scrollView.mj_header?.endRefreshing()
            case .endFooterRefresh:
                scrollView.mj_footer?.endRefreshing()
            case .endAllRefresh:
                // 結束全部拉重新整理
                scrollView.mj_header?.endRefreshing()
                scrollView.mj_footer?.endRefreshing()
            case .footerStatus(let isHidden, let isNone):
                // 根據關聯值確定 footer 的狀態。
                scrollView.mj_footer?.isHidden = isHidden
                // 處理尾部狀態時,如果之前正在重新整理頭部,則結束重新整理,
                // 至此,我們無需寫判斷結束頭部重新整理的程式碼,在這裡自動處理。
                scrollView.mj_header?.endRefreshing()
                if isNone {
                    scrollView.mj_footer?.endRefreshingWithNoMoreData()
                }else {
                    scrollView.mj_footer?.endRefreshing()
                }
            }
        })
    }
}
複製程式碼

  給 Refreshable 加一個擴充套件,我們先來看方法的第一部分:建立頭部和尾部重新整理控制元件。這段程式碼很容易看懂,結合前面放在 ViewController.swift 裡的程式碼:

// 兩個閉包的引數預設為 nil,根據引數自動建立 mj_header 或 mj_footer,
// 不傳引數則不建立。自動管理重新整理狀態。
viewModel.refreshStatusBind(to: tableView, {
    // 處理頭部重新整理。
}) {
    // 處理尾部重新整理。
}.disposed(by: bag)
複製程式碼

  無非就是傳閉包引數的時候建立對應的重新整理控制元件,並把傳進去的閉包作為控制元件的重新整理事件。因為我已經把 UIScrollView 作為引數傳進來了,所以可以直接拿到它建立重新整理控制元件。

  在到第二部分,這就是一個真正監聽狀態改變的部分了,是根據發出的訊息做出改變的反應。這樣,我們在 ViewModel 裡傳送訊息,這裡就能接收到並作出對應的改變。

Demo

  貼下完整程式碼。

ViewController.swift

import UIKit
import RxSwift

class ViewController: UITableViewController {

    lazy var viewModel = ViewModel()
    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        /// 建立重新整理控制元件。
        viewModel.refreshStatusBind(to: tableView, { [weak self] in
            // 處理頭部重新整理。
            self?.viewModel.reload.onNext(false)
        }) { [weak self] in
            // 處理尾部重新整理。
            self?.viewModel.reload.onNext(true)
        }.disposed(by: bag)
        
        // 給 viewModel 中的 reload 傳送訊息,讓其請求資料。
        // 引數 Bool 表示是否上拉。
        viewModel.reload.onNext(false)
    }
}
複製程式碼

ViewModel.swift

import UIKit
import RxSwift

class ViewModel: Refreshable {

    lazy var list = Variable<[Model]>([])
    let refreshStatus = BehaviorSubject(value: RefreshStatus.none)
    let reload = PublishSubject<Bool>()
    let bag = DisposeBag()

    init() {
        reload.subscribe(onNext: { [weak self] (isReload) in
        
            guard let `self` = self else {
                return
            }
            
            MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in
            
                let list = result.value?["list"].arrayObject
                let models = decode([MnlDakaCommentModel].self, from: list) ?? []
                
                self.list.value = isReload ? models : self.list.value + models
                
                let count = result.value?["count"].int ?? 0
                // 傳送重新整理狀態給訂閱者,讓其作出改變。
                // 如果列表個數和總數相等,則判斷它為沒有更多資料。
                self.refreshStatus.onNext(.footerStatus(isHidden: self.list.value.isEmpty,
                                                        isNoMoreData: self.list.value.count == count))
            }
        }).disposed(by: bag)
    }
}
複製程式碼

  關於 DisposeBag,其實就是一個資源回收包, 使用 Rx 程式碼會佔用一些資源,我們把這些資源都新增到 bag 裡面,這樣在其對應的引用被 deinit 後,資源會被回收。

  詳情可以查閱官方文件: Disposing section

相關文章