RxSwift - Start using it today! The practical introduction
My journey with FRP has started with ReactiveCocoa v2.4 which was the most popular library implementing concepts of Functional Reactive Programming in Objective-C. At the beginning, I had a shallow understanding of ReactiveCocoa so I limited the usage only for listening for UI events (like button taps) or handling REST API calls. It was a fantastic approach because later it was much easier for me to understand the idea which stands behind RxSwift.
This is it what I want to show you. Without diving into details of RxSwift and what a stream, sequence or Observable is I would like to demonstrate you the best use cases how FRP can simplify your ordinary problems. It’s a stretch before a long rx-run đ
Replace target & action with RxSwift
I donât like to subscribe for UIControl events by passing a target and a selector. In Objective-C world, one change during the refactor could end up with a runtime crash later. Although Swiftâs compiler checks also if the selector exists, I still think it is a good idea to replace target & action with closures. Due to Swiftâs extensions, RxCocoa adds handy properties into existing UIKit classes. We donât need to create Rx
subclasses of UIViewâs as Android developers have to đ
The usage of RxCocoa could not be simpler! To get the information about changes inside a UITextField you just need to subscribe for UITextFieldâs rx.text
property:
textField.rx.text.subscribe(onNext: { [weak self] text in
self?.search(withQuery: text);
}).addDisposableTo(disposeBag)
What about listening for button taps? It is also easy, thanks to rx.tap
property:
button.rx.tap.subscribe(onNext: { [unowned self] in
if self.isFollowedByMe() {
self.follow()
} else {
self.unfollow()
}
}).addDisposableTo(disposeBag)
Every subscribe
creates a retain cycle inside Rxâs logic. Thanks to it you donât have to keep a strong reference to the button.rx.tap
observable in above example. However, you have to break the retain cycle at some point. To do so, you have to call dispose()
on Disposable
which is an output from subscribe
. Usually, you have more than a one Rx subscription inside UIViewController
and to make disposing easier, you can add any Disposable
into a DisposeBag
. Itâs just an array of Disposables
which, on dealloc, goes through all disposables and dispose them. Disposables are disposed when the DisposeBag
is deallocated which deallocs when its owner is deallocated.
The debounce operator
The functional word in FRP stands there for a reason. RxSwift has set of multiple operators – functions defined inside ObservableType
which return another Observable
. One of the simplest, and yet powerful, operator is a debounce
. The debounce
takes 2 arguments, the time interval and a scheduler:
textField.rx.text
.debounce(0.3, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] text in
self?.search(withQuery: text);
}).addDisposableTo(disposeBag)
button.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] in
if self.isFollowedByMe() {
self.follow()
} else {
self.unfollow()
}
}).addDisposableTo(disposeBag)
Iâve jest add debounce(0.3, scheduler: MainScheduler.instance)
operator into previous examples. What has changed? In the case of UITextField, debounce
tells Rx that you want to be notified about a text from the text field if the text wasn’t changed in last 0.3 seconds. It means if a user will be constantly writing Rx will send you an event into subscribe(onNext:)
after 0.3-second break in writing. As a result, it solves a common problem when you donât want to ask your REST API for search query results every time a user writes something, but only then when he âfinishedâ writing.
What about button taps? Imagine your app have a like button, similar to a heart button on Twitter. Usually, when a user would like to like something, he will just press the like button once. Does your QA act the same? No! He taps-taps-taps-taps-taps-taps like a crazy :D. In this case, debounce
will also protect you from sending multiple events to the API ;).
Schedulers are the abstraction how RxSwift handles concurrency, threading and dispatching actions into queues. In case of debounce
use MainScheduler. It will dispatch the event into the main queue. Concurrency was, is and will be a complicated aspect of programming and for this reason, I want to dedicate a separate article for Schedulers. However, until that happens I can recommend you this article about schedulers.
Button taps & UITableView/UICollectionView
Subscribing for a UIButton
taps is great with reusable cells. While using target and action you have to magically receive IndexPath
for recent touch. Thankfully, Rx uses closures so you have a reference to the IndexPath
immediately:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DummyCell" for:indexPath)
cell.button.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] in
self.like(postAt: indexPath)
}).addDisposableTo(cell.rx_reusableDisposeBag)
}
In the case of reusable cells, there is one thing to remember. You have to invoke addDisposableTo
with cellâs disposeBag, not with a dataSourceâs bag. Moreover, you have to recreate DisposeBag
when the cell is reused. It is very important! This is how I handle that:
class RxCollectionViewCell: UICollectionViewCell {
private (set) var rx_reusableDisposeBag = DisposeBag()
override func prepareForReuse() {
rx_reusableDisposeBag = DisposeBag()
super.prepareForReuse()
}
}
Iâll repeat myself, it is very important to not forget about using cellâs disposeBag. Otherwise, you will get multiple events for just one button tap. Luckily, there is handy library which adds rx_reusableDisposeBag
into all reusable type views.
Use Rx for handling notification
RxSwift also allows you to handle notification from NotificationCenter by using closures:
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
_ = NotificationCenter.default.rx
.notification(NSNotification.Name.UIKeyboardDidShow)
.takeUntil(rx.methodInvoked(#selector(viewWillDisappear(_:))))
.subscribe(onNext: { notification in
self.doSmthWithKeyboard(notification)
});
}
Usually, when you add an observer into NotificationCenter
you register it inside viewWillAppear
and unregister in viewWillDisappear
. To do the same with Rx, you have to use takeUntil
operator. This time you donât need to add the Disposable
into DisposableBag
, hence the takeUntil
will invoke the dispose
when the viewWillDisappear
will be called.
RxAPI
I saved this to the end. This is the case where the Rx shines mostly. Sometimes you have to make at least 2 API calls to render one screen in consequence of not all REST APIs are made for mobile. Doing this in an imperative way can be tricky. You keep some booleans to mark if requests are finished not to mention creating a class which synchronizes all those flags.
How Rx can help you? First of all, you have to wrap your API call within Observable
:
class HTTPClient {
func firstResource(_ parameter: Int, callback: @escaping (Result<String>) -> Void) -> DataRequest
func secondResource(callback: @escaping (Result<String>) -> Void) -> DataRequest
}
//1
extension HTTPClient: ReactiveCompatible {}
extension Reactive where Base: HTTPClient {
func firstResource(_ parameter: Int) -> Observable<String> {
//2
return Observable.create { observer in
//3
let reqeust = self.base.firstResource(parameter, callback: self.sendResponse(into: observer))
//5
return Disposables.create() {
reqeust.cancel();
}
}
}
func secondResource() -> Observable<String> {
return Observable.create { observer in
let reqeust = self.base.secondResource(callback: self.sendResponse(into: observer))
return Disposables.create() {
reqeust.cancel();
}
}
}
//4
func sendResponse<T>(into observer: AnyObserver<T>) -> ((Result<T>) -> Void) {
return { result in
switch result {
case .success(let response):
observer.onNext(response)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
}
}
}
Initially, you have to create Rx version of the existing function. The rule of thumb is to name the functions the same as their equivalent with rx_
prefix or to create an extension for Reactive
struct where Base
is your class (1) and add functions with the exact same name. The function takes arguments as the original one and returns Observable<T>
where T
is a parameter type of success response. Afterward use Observable.create
to create an Observable (2). Finally, invoke original function inside Observable.create
closure (3). When the response come you have 2 cases to handle:
- If request returns a success always invoke
observer.onNext(<T>)
andobserver.onCompleted()
- If request returns an error just invoke
observer.onError(<ErrorType>)
(4)
Good practice is to cancel the request when the Observable is destroyed. To do so you have to returnDisposable
with closure (5).
The zip operator
To get back to the point how Rx can help you. Now, when you have Rx
version of your API requests you can just use zip
operator to chain those two requests. Zip
operator combines 2 observables and send you a response when both API requests are finished.
Observable.zip(httpClient.rx.firstResource(1), httpClient.rx.secondResource()) { ($0, $1) }
.subscribe(onNext: { response1, response2 in
print(response1, response2, separator:"\n")
}).addDisposableTo(disposeBag)
}
The retry
It’s common approach to retry if API request fails. It’s a trivial task with the retry
operator. All what you have to do is just add one line with .retry(1)
. The integer parameter means how many times do you want to retry the API Call. What is more, The RxSwiftExt adds possibility to retry with exponential growth delay:
func secondResource() -> Observable<String> {
return Observable.create { observer in
let reqeust = self.base.secondResource(callback: self.sendResponse(into: observer))
return Disposables.create() {
reqeust.cancel();
}
}.retry(.exponentialDelayed(maxCount: 3, initial: 2, multiplier: 1))
}
Now, the app will try to retry the request if it fails. First attempt will occur after 2 seconds, next after 4 seconds from previous one and the last one after 8 seconds.
Conclusion
Is Functional Reactive Programming for mobile developers? Definitely yes, because our work is all about reacting for UI events. Unifying how you handle target-action and notifications improve readability. Furthermore, RxSwift also simplifies non-trivial use cases such as synchronizing 2 requests or implementing the retry with exponential growth delay. With RxSwift is a matter of 1 line of code â¤ď¸.
Stay tuned for the next article when we go deeper into the logic which stands behind Rx.
Write a comment below if thereâs anything unclear for you. Iâll be glad to help you! If you like the article share it with your friends to persuade them to start using Rx!
All code snippets are available in the sample project.