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