Rx for the Ultimate Beginner – Part 3 (Swift)

This is the third part of our ongoing “Rx for the Ultimate Beginner” series. If you haven’t read the previous ones, you can find the outline at the beginning of the first post. This post has a twin sibling for Android developers with code examples given in Kotlin.

A Small Recap

In the last post, we talked about different kinds of observables, namely hot and cold observables. If the source is capable of producing values even if no subscriber is connected and a subscription causes no side effects at the source, we call this a “hot” observable. If the  existence of subscribers determines the production of emissions and subscriptions can cause side effects at the source, we call this a “cold” observable.

We also explored the Observable Contract. This contract specifies that during the lifetime of a subscription, a subscriber can observe many consecutive onNext calls, and a potential onComplete or onError depending on the result. A subscription which observes an onComplete or an onError is considered finalized and cannot observe any other emissions.

These guidelines will become useful when we implement our own observables.

Our First Observables

Observables are created via creation operators. There are many specialized implementations for creating timers, sequences, and ranges. In our case, we are going to start with the least magical one, namely Observable.create().

In order to keep the example concrete, we are going to wrap an HTTP call made with Alamofire inside of an observable. Here is the call as documented in the Alamofire documentation, without the wrapping:

Alamofire.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseData { response in
        switch response.result {
        case .success:
            print("Validation Successful")
        case .failure(let error):
            print(error)
        }
    }

This piece of code makes an async call that has some kind of chaining structure built in for better usability. This may also be an async call to a background thread or some other library that accepts callbacks to notify its results. In our case, the wrapping process would be the same.

Let’s wrap it:

let httpCallObservable:Observable<Any> = Observable<Any>.create { sub in
    Alamofire.request("https://httpbin.org/get")
        .validate(statusCode: 200..<300)
        .validate(contentType: ["application/json"])
        .responseJSON { response in
            switch response.result {
            case .success:
                sub.onNext(response.result.value!)
                sub.onCompleted()
            case .failure(let error):
                sub.onError(error)
            }
    }
    
    return Disposables.create()
}

What we did here is actually quite easy. We called Observable<Any>.create() which accepts a block to define what to do when a subscriber subscribes. We used Observable<Any> because Alamofire’s reponse.result.value has the type Any. If you plan to emit other kinds of values inside your onNext calls, specify the type accordingly.

The block we provide is not called unless a subscriber subscribes. In other words, every time a subscriber subscribes, the call will be invoked again. We now have a reusable HTTP service. The subscriber is passed as a single parameter to the block. It has the contract methods we discussed earlier.

Inside the case .success, we called sub.onNext with the response data and sub.onCompleted since this HTTP call has served its purpose. If an error occurs, case .failure will trigger and without any emission, only sub.onError will happen. The contract is obeyed.

Finally, we returned a disposable to mark the things that should be garbage collected once the subscription ends. In our case, we have nothing to dispose of, so our dispose bag is empty. If you have resources to release like file handlers, sockets etc, you can create a disposable with a block consisting of your release calls. Check out Disposables.create overloads to find the one you need.

In order to make the call happen, we need to subscribe:

_ = httpCallObservable.subscribe(
    onNext: { data in print(data) },
    onError: { error in print(error) },
    onCompleted: nil,
    onDisposed: nil
)

Great! We have successfully created our first observable. If we check whether or not we have created a side effect on the source (which is https://httpbin.org/get), it is arguable that we have . But at least we know that this observable can only emit values if someone subscribes, so it is clear by the first guideline that this observable is cold.

Now, let’s create a hot one:

let screenshotNotification = NotificationCenter.default.rx.notification(NSNotification.Name.UIApplicationUserDidTakeScreenshot)

_ = screenshotNotification.subscribe(
    onNext: { notification in print(notification) },
    onError: { error in print(error) },
    onCompleted: { print("this is never called") },
    onDisposed: nil
)

RxSwift comes with useful extension helpers to observe existing controls or broadcasters. In this example, we used NotificationCenter extensions to observe screenshot notifications.

This observable is hot because our subscription has no effect on user’s behavior, they can capture screenshots even if we don’t listen for them. Moreover, no matter how many times we subscribe, all subscriptions will receive the same notification for each screenshot taken. We are merely observers of the outside world, thus it is called hot.

How to know what to use?

From now on, as an Rx user, the main question you are going to ask is “Is this an async event that can be implemented with Rx?”. If the answer is yes, chances are it is already implemented in the library. RxSwift comes with almost all the operators available in the ReactiveX website. It also comes with RxCocoa, which has extension methods for UIViewUIButton and many other UIKit and Foundation classes such as NotificationCenter. If you want to subscribe to gesture events, RxGesture is your friend. Many libraries that have nothing to do with Rx also have  extension pods to add support for Rx. Even Alamofire has one which we have ignored for the  purposes of this post.

In the next post, we are going to learn how to chain multiple operators and the backbone of Rx, the flatMap operator.

Until then, peace!

Code

References