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:

  1. If request returns a success always invoke observer.onNext(<T>) and observer.onCompleted()
  2. 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 return Disposable 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.

You Might Also Like