插播一條小廣告.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