MJRefresh和RxSwift

插播一條小廣告.orz

??我的個人項目: iOS仿寫有妖氣漫畫(組件化架構+響應式編程) 已經(jīng)正式啟動啦.jpg隐孽。
??Update 2019-04-11:有妖氣漫畫這個項目因為結構不是很好,暫時先擱置了神年。小伙伴們需要MVVM+RxSwift源碼的可以看我這兩篇文章。

關于RxSwift

??對RxSwift不熟悉的同學可以查看這兩篇文檔:

這兩篇文檔翻譯的都非常好专钉,小伙伴們多多練習多多體會每個操作符宛琅,Rx系列其實也并不是那么難學(打不開的同學可以問我要電子書)。

MJRefresh的窘境

??MJRefresh相信從事iOS開發(fā)的小伙伴們都很熟悉了午衰,是由李明杰老師開源的下拉刷新上拉加載的第三方庫。它使用的是cocoa中非常常見的target-action模式冒萄。先來看一眼傳統(tǒng)的使用方式:

    // 初始化一個header
    tableView.mj_header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(loadData))

    // 設置刷新的回調(diào)
    @objc func loadData() {
        // 發(fā)起網(wǎng)絡請求臊岸,balabala...
    }

這種使用方式在經(jīng)典的MVC架構下并沒有太多問題(MVC結構下網(wǎng)絡層代碼無處安放,只有ViewController稍微合適尊流,這塊的內(nèi)容網(wǎng)上大書特書帅戒,我就不瞎BB了)。

??而在MVVM結構下崖技,網(wǎng)絡請求相關邏輯被移入了ViewModel逻住。稍微扯幾句MVVM钟哥,MVVM下View是知道ViewModel的,因為要執(zhí)行數(shù)據(jù)綁定更新UI瞎访,而ViewModel是不知道View的腻贰,否則耦合就比較嚴重,ViewModel不能獨立測試装诡,MVVM的優(yōu)勢就蕩然無存了银受。
??接著上面的話題践盼,傳統(tǒng)的使用方式上:

    @objc func loadData() {
        // 發(fā)起網(wǎng)絡請求鸦采,balabala...
        API.loadData(success: { (responseObj) in
            // 1. 處理返回的數(shù)據(jù)
            //   balabala...
            // 2. 關閉mj_header/mj_footer的刷新狀態(tài)
            self.tableView.mj_header.endRefreshing()
        }, failure: { (error) in
            // 1. 處理錯誤
            //   balabala...
            // 2. 同樣要關閉刷新狀態(tài)
            self.tableView.mj_header.endRefreshing()
        })

Command+R運行良好,可以泡杯茶休息一下了~~
慢著慢著咕幻,如果是MVVM渔伯,那么在ViewModel中就會是:

    static func loadData() -> Observable<Data> {
        return Observable<Data>.create({ (observer) in
            let task = URLSession.shared.dataTask(with: URLRequest(url: URL(string: "http://balabala...")!), completionHandler: { (data, _, error) in
                // 處理出錯
                guard error != nil else {
                    observer.onError(error!)
                    return
                }
                // 處理出錯
                guard let data = data else {
                    observer.onError(NSError(domain: "com.archer.errorDomain", code: 250, userInfo: nil))
                    return
                }
                // 請求成功 返回數(shù)據(jù)
                observer.onNext(data)
                observer.onCompleted()
            })
            task.resume()
            return Disposables.create { task.cancel() }
        })
    }

那么問題來了,ViewModel中是不能持有View的肄程,那么在這里就不能直接停止mj_header/mj_footer的刷新狀態(tài)锣吼。又要返回View Controller在訂閱的地方處理嗎?像這樣蓝厌?

        API.loadData()
            .subscribe(onNext: { (data) in
                // 處理數(shù)據(jù)
                // balabala...
            }, onError: { (error) in
                // 1. 處理出錯
                //   balabala...
                // 2. 停止刷新
                self.tableView.mj_header.endRefreshing()
            }, onCompleted: {
                // 停止刷新
                self.tableView.mj_header.endRefreshing()
            }).disposed(by: disposeBag)

對這個簡單的請求來說可以是可以玄叠,可是這一點也不Rx。如果是一個返回給RxTableViewSectionedReloadDataSource的Observable<SectionModel>呢拓提?

        API.loadData() // 假設返回Observable<SectionModel>
            .bind(to: tableView.rx.items(dataSource: dataSouce))
            .disposed(by: disposeBag)

emmm...沒有地方處理刷新控件的狀態(tài)了读恃。聰明的你又想到了再寫一遍API.loadData().subscribe去處理。zzZ~~簡單來說這樣會觸發(fā)兩次網(wǎng)絡請求代态,因為Rx本身并不保持狀態(tài)寺惫,你需要這樣:

        // 使用share操作符來共享狀態(tài)
        let mObservable = API.loadData().share(replay: 1)
        mObservable
            .bind(to: tableView.rx.items(dataSource: dataSouce))
            .disposed(by: disposeBag)
        mObservable
            .subscribe(onNext: { (data) in
                // ...
            }, onError: { (error) in
                // ...
            }, onCompleted: {
                // ...
            }).disposed(by: disposeBag)

好吧,這樣的代碼已經(jīng)和優(yōu)雅不沾邊了蹦疑。

RxSwift結合MJRefresh

??廢話了半天西雀,終于引出我們的主角了。簡單總結一下我們的需求:在用戶下拉tableView到一定距離歉摧,MJRefresh通知我們它已經(jīng)進入刷新狀態(tài)艇肴,此時可以去發(fā)起請求了,在請求成功結束或失敗的時候叁温,我們通知MJRefresh結束其刷新狀態(tài)再悼,這樣就完成了一次具體下拉刷新操作。
??查看一下MJRefresh的源碼券盅,MJRefreshHeader和MJRefreshFooter均繼承自MJRefreshComponent帮哈,在MJRefreshComponent中定義了一個枚舉:

/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀態(tài) */
    MJRefreshStateIdle = 1,
    /** 松開就可以進行刷新的狀態(tài) */
    MJRefreshStatePulling,
    /** 正在刷新中的狀態(tài) */
    MJRefreshStateRefreshing,
    /** 即將刷新的狀態(tài) */
    MJRefreshStateWillRefresh,
    /** 所有數(shù)據(jù)加載完畢,沒有更多的數(shù)據(jù)了 */
    MJRefreshStateNoMoreData
};

/** 刷新狀態(tài) 一般交給子類內(nèi)部實現(xiàn) */
@property (assign, nonatomic) MJRefreshState state;

就是這個state控制了整個刷新控件的狀態(tài)锰镀,實例方法beginRefreshing(), endRefreshing(), endRefreshingWithNoMoreData()均是改變state屬性娘侍。

#pragma mark 進入刷新狀態(tài)
- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在刷新咖刃,就完全顯示
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    } else {
        // 預防正在刷新中時,調(diào)用本方法使得header inset回置失敗
        if (self.state != MJRefreshStateRefreshing) {
            self.state = MJRefreshStateWillRefresh;
            // 刷新(預防從另一個控制器回到這個控制器的情況憾筏,回來要重新刷新一下)
            [self setNeedsDisplay];
        }
    }
}

#pragma mark 結束刷新狀態(tài)
- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

- (void)endRefreshingWithNoMoreData
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateNoMoreData;
    });
}

??MJRefreshComponent的子類都是根據(jù)這個state來改變自身狀態(tài)嚎杨。明白了原理,接下來的目標就相對明確了氧腰。我們需要這個state通知我們何時發(fā)起請求枫浙,又需要通知這個state結束刷新,因此它需要同時是Observable和Observer古拴。RxCocoa中為我們提供的ControlProperty剛好滿足這個需求箩帚。
??翻閱一下RxCocoa,UITextFiled的rx.text屬性就實現(xiàn)為ControlProperty黄痪,讓我們看一下它是怎么實現(xiàn)的:

    public func controlProperty<T>(
        editingEvents: UIControlEvents,
        getter: @escaping (Base) -> T,
        setter: @escaping (Base, T) -> ()
    ) -> ControlProperty<T> {
        // 創(chuàng)建Observable
        let source: Observable<T> = Observable.create { [weak weakControl = base] observer in
                // base被銷毀就結束流
                guard let control = weakControl else {
                    observer.on(.completed)
                    return Disposables.create()
                }
                // 發(fā)出初始值
                observer.on(.next(getter(control)))

                let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
                    if let control = weakControl {
                        // editingEvent觸發(fā)時發(fā)出下一個值
                        observer.on(.next(getter(control)))
                    }
                }
                
                return Disposables.create(with: controlTarget.dispose)
            }
             // 流的生命周期和base一致
            .takeUntil(deallocated)

        let bindingObserver = Binder(base, binding: setter)

        return ControlProperty<T>(values: source, valueSink: bindingObserver)
    }

最后的實現(xiàn)為這么一個泛型函數(shù)紧帕,傳遞的editingEvent是[.allEditingEvents, .valueChanged]。函數(shù)內(nèi)部首先創(chuàng)建了一個Observable<T>桅打,泛型參數(shù)T對于UITextFiled的rx.text屬性來說是String?是嗜。創(chuàng)建Observable的過程中保持了一個對調(diào)用者自身的弱引用來避免循環(huán)引用,接著首先檢查調(diào)用者是否被銷毀挺尾,如果被銷毀直接結束流鹅搪,如果沒有就創(chuàng)建一個ControlTarget來接收傳遞的editingEvent≡馄蹋看一下ControlTarget的源碼丽柿,它做的事情很簡單,說白了它就是一個接收事件的target掂僵,回調(diào)的selector把事件轉發(fā)給了初始化參數(shù)Callback航厚。每當editingEvent觸發(fā)時,它都發(fā)出一個值锰蓬,對UITextFiled來說就是取出它當前的text發(fā)出去(通過gette來包裝)幔睬。Binder就更簡單了,每當有新值時芹扭,通過setter設置新值也就是設置UITextFiled的text麻顶。
??整個流程理清了以后,實現(xiàn)RxMJRefresh就很簡單了舱卡,直接上代碼辅肾。

// RxTarget類并不是公開API 我們自己實現(xiàn)一下就好了
class Target: NSObject, Disposable {
    private var retainSelf: Target?
    override init() {
        super.init()
        self.retainSelf = self
    }
    func dispose() {
        self.retainSelf = nil
    }
}

// 自定義target,用來接收MJRefresh的刷新事件
private final
class MJRefreshTarget<Component: MJRefreshComponent>: Target {
    weak var component: Component?
    let refreshingBlock: MJRefreshComponentRefreshingBlock
    
    init(_ component: Component , refreshingBlock: @escaping MJRefreshComponentRefreshingBlock) {
        self.refreshingBlock = refreshingBlock
        self.component = component
        super.init()
        component.setRefreshingTarget(self, refreshingAction: #selector(onRefeshing))
    }
    
    @objc func onRefeshing() {
        refreshingBlock()
    }
    
    override func dispose() {
        super.dispose()
        self.component?.refreshingBlock = nil
    }
}

// 擴展Rx 給MJRefreshComponent 添加refresh的rx擴展
extension Reactive where Base: MJRefreshComponent {
    var refresh: ControlProperty<MJRefreshState> {
        let source: Observable<MJRefreshState> = Observable.create { [weak component = self.base] observer  in
            MainScheduler.ensureExecutingOnScheduler()
            guard let component = component else {
                observer.on(.completed)
                return Disposables.create()
            }

            // 發(fā)出初始值MJRefreshStateIdle
            observer.on(.next(component.state))

            let observer = MJRefreshTarget(component) {
                //  在用戶下拉時 發(fā)出MJRefreshComponent 的狀態(tài)
                observer.on(.next(component.state))
            }
            return observer
            }.takeUntil(deallocated)
        
        // 在setter里設置MJRefreshComponent 的狀態(tài) 
        // 當一個Observable<MJRefreshState>發(fā)出轮锥,假如這個state是MJRefreshStateIdle矫钓,那么MJRefreshComponent 就會結束刷新
        let bindingObserver = Binder<MJRefreshState>(self.base) { (component, state) in
            component.state = state
        }
        return ControlProperty(values: source, valueSink: bindingObserver)
    }
}

幾乎就是照葫蘆畫瓢了~~
再來預習一下使用:

    func bind(reactor: ViewControllerReactor) {
       // 如果發(fā)出一個refreshing事件,就發(fā)起請求
        // 這里就是用戶下拉tableview了
        tableView.mj_header
            .rx.refresh
            .filter { $0 == .refreshing }
            .map { _ in Reactor.Action.refresh }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        // 點擊按鈕轉換成發(fā)出refreshing事件 refreshing已綁定到Reactor.Action.Refresh
        // 觸發(fā)mj_header刷新 然后請求數(shù)據(jù)
        navigationItem.rightBarButtonItem?.rx.tap
            .map { MJRefreshState.refreshing }
            .bind(to: tableView.mj_header.rx.refresh)
            .disposed(by: disposeBag)
        
        // 綁定tableview數(shù)據(jù)源
        reactor.state
            .map { $0.sectionModels }
            .bind(to: tableView.rx.items(dataSource: dataSouce))
            .disposed(by: disposeBag)
        
        // 根據(jù)返回的狀態(tài)控制mj_header的狀態(tài)
        reactor.state
            .map { $0.refreshingState }
            .bind(to: tableView.mj_header.rx.refresh)
            .disposed(by: disposeBag)
    }

??這里使用了ReactorKit而不是MVVM,關于ReactorKit大家可以去Github上看看不難使用新娜。最后附上代碼MJRefresh+Rx赵辕。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市概龄,隨后出現(xiàn)的幾起案子还惠,更是在濱河造成了極大的恐慌,老刑警劉巖私杜,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚕键,死亡現(xiàn)場離奇詭異,居然都是意外死亡衰粹,警方通過查閱死者的電腦和手機锣光,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寄猩,“玉大人嫉晶,你說我怎么就攤上這事√锲” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵箍铭,是天一觀的道長泊柬。 經(jīng)常有香客問我,道長诈火,這世上最難降的妖魔是什么兽赁? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮冷守,結果婚禮上刀崖,老公的妹妹穿的比我還像新娘。我一直安慰自己拍摇,他們只是感情好亮钦,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著充活,像睡著了一般蜂莉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上混卵,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天映穗,我揣著相機與錄音,去河邊找鬼幕随。 笑死蚁滋,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辕录,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼澄阳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了踏拜?” 一聲冷哼從身側響起碎赢,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎速梗,沒想到半個月后肮塞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡姻锁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年戒劫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颖低。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡俭茧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涧黄,到底是詐尸還是另有隱情篮昧,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布笋妥,位于F島的核電站懊昨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏春宣。R本人自食惡果不足惜酵颁,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望月帝。 院中可真熱鬧躏惋,春花似錦、人聲如沸嚷辅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽潦蝇。三九已至款熬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間攘乒,已是汗流浹背贤牛。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留则酝,地道東北人殉簸。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓闰集,卻偏偏與公主長得像,于是被迫代替她去往敵國和親般卑。 傳聞我的和親對象是個殘疾皇子武鲁,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容