天天看點

MJRefresh和RxSwift

插播一條小廣告.orz

  我的個人項目: iOS仿寫有妖氣漫畫(元件化架構+響應式程式設計) 已經正式啟動啦.jpg。

關于RxSwift

  對RxSwift不熟悉的同學可以檢視這兩篇文檔:

  • RxSwift中文文檔
  • ReactiveX中文文檔

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

MJRefresh的窘境

  MJRefresh相信從事iOS開發的小夥伴們都很熟悉了,是由李明傑老師開源的下拉重新整理上拉加載的第三方庫。它使用的是cocoa中非常常見的target-action模式。先來看一眼傳統的使用方式:

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

    // 設定重新整理的回調
    @objc func loadData() {
        // 發起網絡請求,balabala...
    }
複制代碼
           

這種使用方式在經典的MVC架構下并沒有太多問題(MVC結構下網絡層代碼無處安放,隻有ViewController稍微合适,這塊的内容網上大書特書,我就不瞎BB了)。

  而在MVVM結構下,網絡請求相關邏輯被移入了ViewModel。稍微扯幾句MVVM,MVVM下View是知道ViewModel的,因為要執行資料綁定更新UI,而ViewModel是不知道View的,否則耦合就比較嚴重,ViewModel不能獨立測試,MVVM的優勢就蕩然無存了。   接着上面的話題,傳統的使用方式上:

@objc func loadData() {
        // 發起網絡請求,balabala...
        API.loadData(success: { (responseObj) in
            // 1. 處理傳回的資料
            //   balabala...
            // 2. 關閉mj_header/mj_footer的重新整理狀态
            self.tableView.mj_header.endRefreshing()
        }, failure: { (error) in
            // 1. 處理錯誤
            //   balabala...
            // 2. 同樣要關閉重新整理狀态
            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
                }
                // 請求成功 傳回資料
                observer.onNext(data)
                observer.onCompleted()
            })
            task.resume()
            return Disposables.create { task.cancel() }
        })
    }
複制代碼
           

那麼問題來了,ViewModel中是不能持有View的,那麼在這裡就不能直接停止mj_header/mj_footer的重新整理狀态。又要傳回View Controller在訂閱的地方處理嗎?像這樣?

API.loadData()
            .subscribe(onNext: { (data) in
                // 處理資料
                // balabala...
            }, onError: { (error) in
                // 1. 處理出錯
                //   balabala...
                // 2. 停止重新整理
                self.tableView.mj_header.endRefreshing()
            }, onCompleted: {
                // 停止重新整理
                self.tableView.mj_header.endRefreshing()
            }).disposed(by: disposeBag)
複制代碼
           

對這個簡單的請求來說可以是可以,可是這一點也不Rx。如果是一個傳回給RxTableViewSectionedReloadDataSource的Observable呢?

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

emmm...沒有地方處理重新整理控件的狀态了。聰明的你又想到了再寫一遍API.loadData().subscribe去處理。zzZ~~簡單來說這樣會觸發兩次網絡請求,因為Rx本身并不保持狀态,你需要這樣:

// 使用share操作符來共享狀态
        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)
複制代碼
           

好吧,這樣的代碼已經和優雅不沾邊了。

RxSwift結合MJRefresh

  廢話了半天,終于引出我們的主角了。簡單總結一下我們的需求:在使用者下拉tableView到一定距離,MJRefresh通知我們它已經進入重新整理狀态,此時可以去發起請求了,在請求成功結束或失敗的時候,我們通知MJRefresh結束其重新整理狀态,這樣就完成了一次具體下拉重新整理操作。   檢視一下MJRefresh的源碼,MJRefreshHeader和MJRefreshFooter均繼承自MJRefreshComponent,在MJRefreshComponent中定義了一個枚舉:

/** 重新整理控件的狀态 */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀态 */
    MJRefreshStateIdle = 1,
    /** 松開就可以進行重新整理的狀态 */
    MJRefreshStatePulling,
    /** 正在重新整理中的狀态 */
    MJRefreshStateRefreshing,
    /** 即将重新整理的狀态 */
    MJRefreshStateWillRefresh,
    /** 所有資料加載完畢,沒有更多的資料了 */
    MJRefreshStateNoMoreData
};

/** 重新整理狀态 一般交給子類内部實作 */
@property (assign, nonatomic) MJRefreshState state;
複制代碼
           

就是這個state控制了整個重新整理控件的狀态,執行個體方法beginRefreshing(), endRefreshing(), endRefreshingWithNoMoreData()均是改變state屬性。

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

#pragma mark 結束重新整理狀态
- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

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

  MJRefreshComponent的子類都是根據這個state來改變自身狀态。明白了原理,接下來的目标就相對明确了。我們需要這個state通知我們何時發起請求,又需要通知這個state結束重新整理,是以它需要同時是Observable和Observer。RxCocoa中為我們提供的ControlProperty剛好滿足這個需求。   翻閱一下RxCocoa,UITextFiled的rx.text屬性就實作為ControlProperty,讓我們看一下它是怎麼實作的:

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

                let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
                    if let control = weakControl {
                        // editingEvent觸發時發出下一個值
                        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)
    }
複制代碼
           

最後的實作為這麼一個泛型函數,傳遞的editingEvent是[.allEditingEvents, .valueChanged]。函數内部首先建立了一個Observable,泛型參數T對于UITextFiled的rx.text屬性來說是String?。建立Observable的過程中保持了一個對調用者自身的弱引用來避免循環引用,接着首先檢查調用者是否被銷毀,如果被銷毀直接結束流,如果沒有就建立一個ControlTarget來接收傳遞的editingEvent。看一下ControlTarget的源碼,它做的事情很簡單,說白了它就是一個接收事件的target,回調的selector把事件轉發給了初始化參數Callback。每當editingEvent觸發時,它都發出一個值,對UITextFiled來說就是取出它目前的text發出去(通過gette來包裝)。Binder就更簡單了,每當有新值時,通過setter設定新值也就是設定UITextFiled的text。   整個流程理清了以後,實作RxMJRefresh就很簡單了,直接上代碼。

// RxTarget類并不是公開API 我們自己實作一下就好了
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()
            }

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

            let observer = MJRefreshTarget(component) {
                //  在使用者下拉時 發出MJRefreshComponent 的狀态
                observer.on(.next(component.state))
            }
            return observer
            }.takeUntil(deallocated)
        
        // 在setter裡設定MJRefreshComponent 的狀态 
        // 當一個Observable<MJRefreshState>發出,假如這個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) {
       // 如果發出一個refreshing事件,就發起請求
        // 這裡就是使用者下拉tableview了
        tableView.mj_header
            .rx.refresh
            .filter { $0 == .refreshing }
            .map { _ in Reactor.Action.refresh }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        // 點選按鈕轉換成發出refreshing事件 refreshing已綁定到Reactor.Action.Refresh
        // 觸發mj_header重新整理 然後請求資料
        navigationItem.rightBarButtonItem?.rx.tap
            .map { MJRefreshState.refreshing }
            .bind(to: tableView.mj_header.rx.refresh)
            .disposed(by: disposeBag)
        
        // 綁定tableview資料源
        reactor.state
            .map { $0.sectionModels }
            .bind(to: tableView.rx.items(dataSource: dataSouce))
            .disposed(by: disposeBag)
        
        // 根據傳回的狀态控制mj_header的狀态
        reactor.state
            .map { $0.refreshingState }
            .bind(to: tableView.mj_header.rx.refresh)
            .disposed(by: disposeBag)
    }
複制代碼
           

  這裡使用了ReactorKit而不是MVVM,關于ReactorKit大家可以去Github上看看不難使用。最後附上代碼MJRefresh+Rx。

轉載于:https://juejin.im/post/5c2dcaaa51882501c1662d46