This is the 21st day of my participation in the Gwen Challenge.More article challenges
Let’s start with an example of RxSwift encapsulating UIButton
All things are easy before they are easy, so before attempting to encapsulate MJRefresh via RxSwift, let’s take a look at the RxCocoa framework for encapsulating system components so that we can copy them.
Write button click events using RxSwift
Let button = UIButton(type:.custom) button.rx.tap. Subscribe {_ in print(" Button click event ")}. Disposed (by: rx.disposebag)Copy the code
Here we can see button.rx.tap handled in this way, and it can be subscribed, indicating that it returns a sequence.
The rx.tap writing style is common in many parts of Swift. As I have read the Kingfisher source code before, I know that this is an extension to the namespace of the class by protocol.
So let’s take a look at how the tap is encapsulated:
extension Reactive where Base: UIButton {
/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
return controlEvent(.touchUpInside)
}
}
Copy the code
About the ControlEvent
ControlEvent It is a specialized sequence.
The ControlEvent is used exclusively to describe the events generated by the UI control. It has the following characteristics:
- No error events are generated
- Be sure to subscribe to MainScheduler
- Must be listening on MainScheduler
- Shared add-on
Since it is a sequence, subscriptions are possible.
Let’s figure out how to encapsulate MJRefresh.
RxSwift encapsulation MJRefresh
Pull-down and pull-up behavior encapsulation
Mj_header tableView.mj_header tableView.mj_header
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
Copy the code
Add a property in UIScrollView via Runtime of type MJRefreshHeader.
So when we start writing extensions, we can name them like this:
Reactive where Base: MJRefreshHeader {var refresh: ControlEvent<Void>}Copy the code
But inside the implementation of how to write, for me a RxSwift novice is a difficult problem, but it doesn’t matter, they can not write, you can go to search, look at the other code, maybe there are ideas.
extension Reactive where Base: MJRefreshHeader {
var refresh: ControlEvent<Void> {
let source: Observable<Void> = Observable.create {
[weak control = self.base] observer in
if let control = control {
control.refreshingBlock = {
observer.on(.next(()))
}
}
return Disposables.create()
}
return ControlEvent(events: source)
}
}
Copy the code
In the above code, source is a sequence, so to generate a sequence, we use the specific factory method Observable. Create {} to get the base, which is the MJRefreshHeader itself.
The MJRefreshHeader callback is then used to manipulate the behavior by the observer, and the ControlEvent initialization method is then used to wrap the ControlEvent sequence.
So we can use it like this:
tableView.mj_header? .rx.refresh. Subscribe {[weak self] _ in // refresh the event self? .refreshAction() }.disposed(by: rx.disposeBag)Copy the code
You might even think it’s less concise than the native Api:
tableView.mj_header? .beginRefreshing { [weak self] in self? .refreshAction() }Copy the code
I think so myself, but in this case, we’re focusing more on encapsulated thinking.
Now that the Mj_header is wrapped, the mj_footer follows suit.
The mj_header/mj_footer inheritance is a bit more concise:
@interface MJRefreshHeader : MJRefreshComponent
@interface MJRefreshFooter : MJRefreshComponent
Copy the code
Since MJRefreshHeader and MJRefreshFooter inherit from MJRefreshComponent, encapsulating them once can be used:
extension Reactive where Base: MJRefreshComponent {
var refresh: ControlEvent<Void> {
let source: Observable<Void> = Observable.create {
[weak control = self.base] observer in
if let control = control {
control.refreshingBlock = {
observer.on(.next(()))
}
}
return Disposables.create()
}
return ControlEvent(events: source)
}
}
Copy the code
Mj_header and mj_footer encapsulate the same behavior:
/// Drop down to refresh tableView.mj_header? .rx.refresh .subscribe { [weak self] _ in self? RefreshAction ()}.prompt (by: rx.disposebag) /// Pull up load tableViewer.mj_footer? .rx.refresh .subscribe { [weak self] _ in self? .loadMoreAction() }.disposed(by: rx.disposeBag)Copy the code
State relation binding
If the above behavior encapsulation, just RxSwift use of icing on the cake, then for the tableView refresh state encapsulation, and binding with tableView, is the core problem of the encapsulation.
The idea is to bind the sequence of wrapped states to the Binder properties defined in the tableView extension.
It took me a long time to figure out the relationship between state and UI transition, and I finally found the following code on the Internet:
/// for ViewModel use enum MJRefreshAction {// start refresh case begainRefresh /// stopRefresh case stopRefresh // start load more cases BegainLoadmore // stop loading more case stopLoadmore // display no more data case showNomoreData // resetNomoreData case resetNomoreData} //MARK:- Refresh Extension Reactive where Base: UIScrollView {Refresh Action: Binder<MJRefreshAction> { return Binder(base) { (target, action) in switch action{ case .begainRefresh: if let header = target.mj_header { header.beginRefreshing() } case .stopRefresh: if let header = target.mj_header { header.endRefreshing() } case .begainLoadmore: if let footer = target.mj_footer { footer.beginRefreshing() } case .stopLoadmore: if let footer = target.mj_footer { footer.endRefreshing() } case .showNomoreData: if let footer = target.mj_footer { footer.endRefreshingWithNoMoreData() } case .resetNomoreData: if let footer = target.mj_footer { footer.resetNoMoreData() } } } } }Copy the code
Use this code as follows:
// Set a state enumeration of refreshSubject in the controller that is both a listener and an observer. BehaviorSubject<MJRefreshAction> = BehaviorSubject(value: BegainRefresh) bind operations / / / / / do/pull-down and pull-up state is bound to the tableView refreshSubject. Bind (to: tableView. Rx. RefreshAction) disposed (by: rx.disposeBag)Copy the code
Then, I carefully read the big guy’s code, abandon their own written this package, directly all use big guy’s code, give big guy knees!!
MJRefresh-RxSwift
Just drag the code from Sources into the project to use.
Code transformation
After using the package written by the big guy, I have modified the points ranking page prepared by RxSwift. The following is the screenshot of the transformation given by Sourcetree. The actual amount of modification is not large:
MJRefresh -rxswift:
import UIKit import RxSwift import RxCocoa import NSObject_Rx import Moya import MJRefresh class RxSwiftCoinRankListController: BaseViewController {/ / / lazy loading tableView private lazy var tableView = UITableView (frame: .zero, style:.plain) /// initialize page as 1 private var page: Int = 1 // the BehaviorSubject private let dataSource is BehaviorSubject private let dataSource: BehaviorRelay<[CoinRank]> = BehaviorRelay(value: []) BehaviorRelay(value: []) BehaviorSubject<MJRefreshAction> = BehaviorSubject(value: .begainRefresh) override func viewDidLoad() { super.viewDidLoad() setupTableView() } private func setupTableView() { /// Set tableFooterView tableView. TableFooterView = UIView () / / / set the proxy tableView. Rx. SetDelegate (self) disposed (by: Mj_header = MJRefreshNormalHeader() tableView.mj_header? .rx.refresh .subscribe { [weak self] _ in self? .refreshAction() }.disposed(by: Rx. DisposeBag) / / / set the tail refresh control tableView. Mj_footer = MJRefreshBackNormalFooter () tableView. Mj_footer? .rx.refresh .subscribe { [weak self] _ in self? .loadMoreAction() }.disposed(by: Rx. DisposeBag) / / / simple layout the addSubview (tableView) tableView. SNP. MakeConstraints {make in the make. Edges. EqualTo (view)} /// dataSource driver dataSource. AsDriver (onErrorJustReturn: []) .drive(tableView.rx.items) { (tableView, row, coinRank) in if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") { cell.textLabel?.text = coinRank.username cell.detailTextLabel?.text = coinRank.coinCount?.toString return cell }else { let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell") cell.textLabel?.text = coinRank.username cell.detailTextLabel?.text = coinRank.coinCount?.toString return cell } Disposed (by: rx.disposeBag) /// Pull down and pull up state bind to tableView refreshSubject.bind (to: tableView.rx.refreshAction) .disposed(by: Rx. DisposeBag)}} the extension RxSwiftCoinRankListController {/ / / the drop-down refresh behavior private func refreshAction () { resetCurrentPageAndMjFooter() getCoinRank(page: Private func loadMoreAction() {page = page + 1 getCoinRank(page: The parameters of the page)} / / / the drop-down and reset behavior private func resetCurrentPageAndMjFooter () {page = 1 tableView. Mj_footer?. IsHidden = false Refresh subject.onNext (.resetnomoredata)} private func getCoinRank(page: Int) {myprovider.rx. request(myservice.coinrank (page)) /// convert model.map (BaseModel< page < coinRank >>.self) /// Because of the need to use the Page, so return to $0. The data of this layer, rather than $0. Data. Datas. The map {$0. Data} / / / unpack compactMap {$0} / / / conversion operations. AsObservable () .assingle () /// subscribe. Subscribe {event in /// subscribe to events /// self.refreshSubject.onNext(.stopRefresh) : self.refreshSubject.onNext(.stopLoadmore) switch event { case .success(let pageModel): If page == 1 {// if page == 1; // if page == 1; // if page == 1 Self.datasource. Accept (self.datasource. Value + datas)}else {// If let curPage = pagemodel.curpage, Let pageCount = pagemodel. pageCount {// if they are found to be equal, it is the last one, change the foot and state if curPage == pageCount { Self. RefreshSubject. OnNext (. ShowNomoreData)}} case. The error (_) : / / / the error of not do processing break}}. Disposed (by: rx.disposeBag) } } extension RxSwiftCoinRankListController: UITableViewDelegate {}Copy the code
conclusion
As I wrote the code here, I started to think about some questions.
Why does Swift have to work so hard to bind data streams?
In Vue, we can easily bind components through state.
In Flutter, we have heard that assigning values to data and refreshing the page through setState affects performance, but is setState at least easy to use?
But when it comes to writing iOS data binding via RxSwift, what do we do? Need to write your own extension!!
Although RxCocoa has extended most system components, its support for third-party libraries is limited. Although I have also seen that Rx has extended very mainstream third-party libraries, such as Moya and Kingfisher, it is difficult for RxSwift to support different apps and developers using different wheels. Developers need to write their own extensions.
Do you know what that means?
Developers need a deep understanding of RxSwift, as well as a deep understanding of this third party (such as the MJRefresh we encapsulate today), to write logical extensions that might not work at all.
This means developers spend more time and effort implementing a feature when it’s easier to just use a good Api. tableView.mj_header? . Rx. Refresh. Subscribe {} with the tableView. Mj_header? .beginrefreshing {} is a good example.
I’m not just blowing my own drum to be an Api engineer. The prerequisite for further learning is to complete the work, and only after completing the work can I have free time to learn. If I am blocked by such a wall in the beginning, how can I further learn?
I even wondered what was holding up the data binding mode in Swift — the lack of convenience right out of the box.
Even with the advent of SwiftUI and Combine, good state management and data-driven features have yet to materialize.
So why am I still learning RxSwift?
Thinking determines coding, learning RxSwift allows me to expand the use of programming, even in other languages, Rx ideas can continue to use, Combine is also the official RxSwift, understand RxSwift will help me, that’s it.
Tomorrow to continue
Ok, now that MJRefresh has been encapsulated using RxSwift, is the page complete?
Have you heard of MVVM? Have you noticed that all of our code is written in the Controller layer?
The next step is to pull the data logic out of the ViewModel layer, so that the Controller simply feeds the user behavior back to the ViewModel layer, and then the ViewModel processes it internally, and then the ViewModel data drives the Controller’s page changes.
Come on, everybody.