Thinking in RxSwift

Thinking in RxSwift

It has been a long time since my last post. I wanted to prepare an article which will cover the theory which stands behind Observable and show the usage on real, not dummy example. It has turned out it was not trivial task as I thought πŸ˜‰

The article is split into 2 parts. At the beginning, I try to explain what is the Observable and how should you treat them.

The second part is a tutorial how to implement a search of Spotify tracks using RxSwift. A theory is a theory, but an example is a thing which makes a subject easier to understand.

To cut a long story short, I hope you will enjoy reading πŸ™‚

Observable – a sequence

The thing which holds people down before jumping into next level while using Rx is the thinking in an imperative way. You have to break old habits and start to think in the Rx way.

I recommend you to think about Observable like about an array. This is because, you can map, filter, reduce or filter the Observable the same way as with an Array. With map you can transform Observable<String> into Observable<Bool> the same as you can map Array<String> into Array<Bool>.

However, there is one huge difference between an Observable and the array.

You know what the square and the cube are, right? How do they refer to each other? The cube is the square with one additional dimension – the depth. I like to describe the Observable that it is an array with one additional dimension: the time.

When you have an Array<String> with 5 elements, you can access to any element you want. All elements are available in time t0.

Array - elements in time
Array – elements in time

On the other hand, when you have an Observable<String>, the first element is available only in time t1, the second in t2, the third in t3 and so on. What’s event more, in tn you are not able to reach for any previous or future elements.

Observable is an array with time dimension
Observable is an array with time dimension

Iterator & Observer pattern

In computer science, there is very commonly used iterator pattern. In Swift the iterator pattern is implemented by IteratorProtocol (it was GeneratorType before Swift 3.0).

The iterator has only one function next(). The iterator allows you to go through all elements in a sequence. Every sequence, like the array, implements IteratorProtocol.

On the other hand, Observable is a mix of two design patterns. The iterator and the observer patterns. The result of mixing those two patterns is that you can listen for "next" events when you deal with the Observable.

You have to admit this is a kind of similarity, isn’t it πŸ˜‰ ?

However, the world is built from small details. What I want to point here is in a case of an array the receiver is in charge when to get the next element.

In a case of the Observable, the producer is in charge when to send the next element. The receiver can only listen to them.

The map example

At the beginning I said you could map the Observable in the same way as you map an array:

let names = ["adam", "daniel", "christian"]
let nameLengths = names.map { $0.characters.count }
print(nameLengths) // "4, 6, 9"

What’s important to notice, nameLengths is a separate array and names didn’t change. The same will happen with Observable:


let disposeBag = DisposeBag()

let backgroundScheduler = SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .userInitiated)

//(1) This Observable emits names in 1 second interval.
let asyncNames: Observable<String> = self.createObservable(from: ["adam", "daniel", "christian"], withTimeInterval: 1)

//(2) map creates new independant Observable<Int>
let asyncNameLenghts: Observable<Int> = asyncNames
    .map { name in
        return name.characters.count
}

asyncNameLenghts.subscribe(onNext: { count in
    print("[\(NSDate())]: \(count)")
}).addDisposableTo(disposeBag)
     
/*
#################################################
[2016-10-31 08:15:27 +0000]: 4
[2016-10-31 08:15:28 +0000]: 6
[2016-10-31 08:15:29 +0000]: 9
*/

map in a case of the array will immediately transform all elements. map on the Observable also transforms all items in a sequence, but the transform will be applied individually when a new event arrives.

Do you see the subscribe method in above example? subscribe is a very important method while working with Observables. subscribe tell the Observable "Hey, I’m ready to listen for your elements". If you forget to call subscribe no events will ever come to you.

Observable – a sequence of events

So far I’ve been using word "next" to call the thing which is sent in an Observable sequence. To be honest, Observables emit events. Next is only a particular type of the enum. Observable can carry 3 types of events, which are: next, error and completed:

public enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
}

I think next is self-explanatory. If you have an Observable<Person>, every next event will carry a Person.

Since Observable is a sequence with a time dimension, an observer doesn’t know the size of the sequence and it means he doesn’t know when to stop listening for events. This is a purpose of error and completed.

Error is sent when something goes wrong and completed is sent when an Observable wants to tell his observers that no more events will be sent.

Please remember another important trait of Observable. error and completed always close an Observable sequence, so it won’t send anything new.

RxMarbles

map is only a common used example from existing operators. Observable has more operators besides it:

  • map
  • flatMap
  • flatMapLatest
  • withLatestFrom
  • filter
  • debounce
  • retry
  • skip
  • catchError
  • take
  • concat
  • merge

The list above represents a set of Rx operators which I use mostly in my daily development. You may say it’s big, but I would say it is still small compared to the set of available operators :D.

To make operators understandable, ReactiveX offers you diagrams called RxMarbles. Those diagrams represent the Observable sequence and how operators affect it. RxMarble for the map example, which I showed you, could look like this:


Marble diagrams legend

Since Observable can emit 3 types of events there are 3 types of marks in RxMarbales which represents next, error and completed:



An alternative way of drawing marbles diagrams is ASCII format. It is in common especially on stackoverflow, forums or slack:

----A--D---C---|->
# map(get string size) #
----4--6---9---|->

---1------X------>

A, B, D, 1, 6, 9 - are next events
| - is the completed event
X - is the error event
--------> - is a timeline

What’s even more, rxmarbles.com is a website where you can manipulate marbles in Observable sequence and see what the result of an operator would be. It is also available as an iPhone app on the App Store. Definitely, it is worth checking out ;).

The Spotify search example

Nothing explains anything so clearly as a good example. My sample covers basic concepts of reactive programming. The task is simple. You have to implement SearchViewController which interacts with Spotify Search API.

To download initial project setup go here

The requirements

To make this tutorial less trivial I want you to cover those requirements which I believe can happen in a real life of iOS developer:

  • Don’t send a request to Spotify every time a user writes a new letter. Wait 0.5 sec until he finishes writing
  • At the beginning preload "Let it go – frozen" as a featured query
  • When user changes the query clear previous search results
  • User is able to refresh current search with pull to refresh
  • The query has to have minimum 3 letters to perform search

RxMantra – Everything can be a sequence

Before writing any code, I would like you to think about sequences and diagrams first. After that, you are able to start thinking about the code. Remember, everything can be a sequence. This is the ReactiveX mantra πŸ˜‰


Requirement #0 – Download & display tracks

First what you have to implement is to download tracks for given query. What you need is to transform the query, which comes from the searchBar, into a set of tracks. Remember, think about sequences first!

In "Observable language", you have to transform a Observable<String>, into Observable<[Track]>. As you can see in the code sample, the TrackCell takes a TrackRenderable as an input to render itself. It means you have to transform Observable<String> -> Observable<[Track]> -> Observable<[TrackRenderable]>:


As you can see,[Track] events are moved in time according to query events. This is because [Track] come from the Spotify API and it needs some time to establish a connection, so the response will come later.

Have you just asked yourself how can you map a query into [Track]?. So far we’ve used map which returns value synchronously, but now you have to transform it asynchronously.

flatMap

I would like to introduce a flatMap operator. flatMap returns an Observable and transfers all it’s events into your original sequence.


Now it is the time to see how the code for those diagrams looks like:

searchBar.rx.text.orEmpty
    .flatMap { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
    }

To display [TrackRenderable] in UITableView you need to bind the observable with the tableView:

searchBar.rx.text.orEmpty
    .flatMap { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
    }.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
        cell.render(trackRenderable: track)
    }.addDisposableTo(disposeBag)            

Requirement #1 – Delay the request

Current Observable sequence transforms query whenever user writes something new. It can be improved by delaying the query event to the time when a user stops writing the whole query. The marble diagram for such requirement will look like that:

searchBarText:     -A-B-C---D-E-F-G-H---I-J----->
delayedQuery:      -----C-----------H-----J----->

Fortunately, RxSwift has a debounce operator which does exactly what above diagram shows. It takes the last event from Observable and sends it if no new event is sent within passing time interval.

The diagram for the whole Observable sequence is as follows:

and the code:

searchBar.rx.text.orEmpty
    .debounce(0.3, scheduler: MainScheduler.instance)
    .flatMap { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
    }.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
        cell.render(trackRenderable: track)
    }.addDisposableTo(disposeBag)            
In my last article I introduced few operators which you will use during this tutorial. I also showed how to wrap API calls within an Observable. If you don’t know how spotifyClient works I encourage you to read the article πŸ˜‰

Stop previous requests

For this moment, you should have SearchViewController which will display tracks when a user writes anything. Congratulations, you have made your first step with RxSwift πŸŽ‰.

Of course, the current implementation is not sufficient to be published on the app store. Unfortunately, there is a kind of a bug inside.

Imagine user wrote "let" and the app has just started downloading tracks. Before the request ends, user finished his intention and user wrote "let it go". The response to "let it go" request arrives before the "let" response.

As a result, the app will display results for "let it go" in the first order. When the "let" response will arrive, the app will display results for the old query which is wrong. Maybe a diagram can be easier to understand:

searchRequest:      --a-b---------c-->
                    ## flatMap {...}
searchResponse:     -----B-A-------C->

a,b,c - requests
A,B,C - responses for corresponding requests (A is response for a)

Use flatMapLatest

flatMapLatest is another cool operator. It is used in replacement of flatMap. The difference between them is flatMapLatest will cancel subscriptions for any previous Observable used inside the closure. Thanks to it, fresh data will never be replaced by the old response:

searchRequest:      --a--b--------c-->
                    ## flatMapLatest {...}
searchResponse:     ------B--------C->

a,b,c - requests
A,B,C - responses for corresponding requests (A is response for a)

In code is a matter of changing flatMap intro flatMapLatest

searchBar.rx.text.orEmpty
    .debounce(0.3, scheduler: MainScheduler.instance)
    .flatMapLatest { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
    }.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
        cell.render(trackRenderable: track)
    }.addDisposableTo(disposeBag)  

Requirement #2 – The featured query

Next requirement says the app should load "Let it go – frozen" results when the view did load.


Always think about diagram first ;):

searchBar.rx.text   ----a---b-c-----d->
                        # ???????????
query               F---a---b-c-----d->
a, b, c, d - string events representing searchBar text
F - featured query

As you probably guess, Rx offers you another operator for that, which is startWith(). startWith() does exactly what you want to do. It takes a parameter and sends it as the initial value of the Observable:

searchBar.rx.text   ----a---b-c-----d->
                        # startWith(F)
query               F---a---b-c-----d->
a, b, c, d - string events representing searchBar text
F - featured query string

o.O the screen is still empty!

UISearchBar, UITextField or UITextView sends an initial value to new subscribers on theirs text Observable. In our case, the searchBar sends an empty string. What happen inside the app is illustrated below:

searchBar.rx.text     ""--a---b-c-----d->
                       # startWith(F)
query               F-""--a---b-c-----d->
a, b, c, d - string events representing searchBar text
"" - empty string
F - featured query

The sequence starts with the featured query, but just after that empty string arrives which loads new, empty response. To prevent this from happening you can use skip(1) operator. It just omits a given number of events:

searchBar.rx.text   ""--a---b-c-----d->
                        # skip(1) 
                    ----a---b-c-----d->
                        # startWith(F)
query               F---a---b-c-----d->
a, b, c, d - string events representing searchBar text
"" - empty string from
F - featured query

Now, when you know what skip does, you can add it to the code:

searchBar.rx.text.orEmpty
    .skip(1)
    .debounce(0.3, scheduler: MainScheduler.instance)
    .startWith("Let it go - frozen")
    .flatMapLatest { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
    }.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
        cell.render(trackRenderable: track)
    }.addDisposableTo(disposeBag)

Requirement #3 – Clearing previous search results

Next improvement which you need to implement is to clear search results when the query has changed.

What does it mean in the case of the Observable sequence which is bound with the tableView? It means when a user writes anything new inside searchBar the Observable<[TrackRenderable]> needs to send empty array:


Look at the diagram above. Observable which runs Spotify API sends events when a user stops writing, because of debounce operator. However, we want to force final Observable<[TrackRenderable]> to sends empty arrays when a user writes anything.


There is a key thing to notice in the diagram above. Look at the last three sequences. trackRenderables seems to be a sum of emptyTracksOnTextChanged and spotifyResponse.

In RxSwift you can combine multiple Observables of the same typo into one Observable. Everything thanks to the merge operator.

Let’s create Observable, which sends empty array of TrackRenderable when user writes anything in searchBar:

let clearTracksOnQueryChanged = searchBar.rx.text.orEmpty
    .skip(1).map { _ in return [TrackRenderable]() }

To be able to use merge operator you have to refactor your current code. Just a little bit:

let tracksFromSpotify: Observable<[TrackRenderable]> = searchBar.rx.text.orEmpty
    .skip(1)
    .debounce(0.3, scheduler: MainScheduler.instance)
    .startWith("Let it go - frozen")
    .flatMapLatest { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
}

tracksFromSpotify.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
            cell.render(trackRenderable: track)
            }.addDisposableTo(disposeBag)

What has changed? I just moved Observable into variable. Now you can use merge:

Observable.of(tracksFromSpotify, clearTracksOnQueryChanged).merge()
.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
            cell.render(trackRenderable: track)
            }.addDisposableTo(disposeBag)

It should work! When a user starts writing a new query, the tableView should be clear.

However, I would like to improve readability and extract merge Observable into a separate variable before binding it with tableView. Moreover, I can see one repetition which I want to get rid of:

let searchTextChanged = searchBar.rx.text.orEmpty.asObservable().skip(1)
let tracksFromSpotify = searchTextChanged
    .debounce(0.3, scheduler: MainScheduler.instance)
    .startWith("Let it go - frozen")
    .flatMapLatest { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
}

let clearTracksOnQueryChanged = searchTextChanged
    .map { _ in return [TrackRenderable]() }

let tracks = Observable.of(tracksFromSpotify, clearTracksOnQueryChanged).merge()

tracks.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
            cell.render(trackRenderable: track)
        }.addDisposableTo(disposeBag)

Requirement #4 – Pull to refresh

Guess what you have to do at the beginning? Yes, you have to think about diagrams first and remember that everything can be a sequence! I’ll repeat that over and over again ;).

Now is the second question. How usually do you implement pull to refresh with search? I guess when a user pulls the tableView you take the current text from a search bar and perform the search.

Am I wrong? πŸ˜‰ – This is exactly the same what you should do with Observables. When a user pulls the tableView, you need to repeat the previous request. Let’s draw some diagrams:

searchBar.rx.text       --a---b----c-----d-----e--->
pullToRefreshObservable --------X-----X------X----->
queryOnPullToRefresh    --------b-----c------d----->
requestObservable       --a---b-b--c--c--d---d-e--->
responseObservable      ----A---B-B--C--C--D--D-E-->

a,b,c,d,e - query from searchBar or request with the query
X - event when a user pulls the tableView
A,B,C,D,E - response for corresponding request with the query

Now when you know what are you looking for go to rxmarbles.com and find the operator what you need ;).

You are back! It means you’ve found withLatestFrom operator which does what diagram above shows.

At the beginning you have to add UIRefreshControl to the tableView. Do so by adding those lines somewhere at the beginning of viewDidLoad:

 let refreshControl = UIRefreshControl()
 tableView.addSubview(refreshControl)

All right, now’s the time to create the Observable which sends events when user pulls the tableView:

let didPullToRefresh: Observable<Void> =  refreshControl.rx.controlEvent(.valueChanged)
    .map { [refreshControl] in
        return refreshControl.isRefreshing
    }.filter { $0 == true }
    .map { _ in return () }

In the code above, I’ve used filter operator. I hope it is understandable by the name. It will ignore all false booleans.

isRefreshing       ---T--F-T---F----T->
            # filter { $0 == T }
didPullToRefresh   ---T----T---T----T->                                                    

Next Observable which you need is an Observable which repeats the last query when a user pulls the tableView:

let refreshWithLastQuery = didPullToRefresh
    .withLatestFrom(searchTextChanged)

It is quite easy, isn’t it? Now you have to combine it with current Observable stack:

let didPullToRefresh: Observable<Void> =  refreshControl.rx.controlEvent(.valueChanged)
    .map { [refreshControl] in
        return refreshControl.isRefreshing
    }.distinctUntilChanged()
    .filter { $0 == true }
    .map { _ in return () }
        
let searchTextChanged = searchBar.rx.text.orEmpty.asObservable().skip(1)
let theQuery = searchTextChanged
    .debounce(0.3, scheduler: MainScheduler.instance)
    .startWith("Let it go - frozen")
        
let refreshLastQuery = didPullToRefresh
    .withLatestFrom(searchTextChanged)
        
let tracksFromSpotify = Observable.of(theQuery, refreshLastQuery).merge()
    .flatMapLatest { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
    }
        
let clearTracksOnQueryChanged = searchTextChanged
    .map { _ in return [TrackRenderable]() }
        
let tracks = Observable.of(tracksFromSpotify, clearTracksOnQueryChanged).merge()
        
tracks.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
    cell.render(trackRenderable: track)
}.addDisposableTo(disposeBag)

Hiding the UIRefreshControl

There is one more case which you need to take into consideration. You need to hide UIRefreshControl when the app ends downloading the tracks.

Observable has a special operator for handling side-effects like hiding the UIRefreshControl. I’m talking about do(next:) operator.

do(next:) won’t return anything from the closure. It just guarantees that the passed closure will be invoked when a next event comes:

    ...
let tracksFromSpotify = Observable.of(theQuery, refreshLastQuery).merge()
    .flatMapLatest { [spotifyClient] query in
        return spotifyClient.rx.search(query: query)
    }.map { tracks in
        return tracks.map(TrackRenderable.init)
    }.do(onNext: { [refreshControl] _ in
        refreshControl.endRefreshing()
    })
    ...        

Run the app and see what you got. You should see working Spotify search with the pull to refresh feature. You have also protected yourself from sending too many requests to the API. Good job πŸ˜‰

Last word & homework

This is it. The tutorial has finished. What I want you to remember after reading the article is what is an Observable. As you’ve read I like to describe an Observable as an array with additional dimension – the time. Important thing is that Observable can emit next, error and completed events.

Reactive programming is all about sequences, events and it forces a new way of thinking. When you have a problem, draw few diagrams first and go to rxmarbles.com to find the required operator.

There is also one requirement which we haven’t implement yet. The query has to have minimum 3 letters to start looking for results. I want you to add this functionality ;). At the end, you can check out my solution which is available here.

Stay tuned!

PS If you like the article please share it with you friends! πŸ™‚
PS2 If you have any questions or you want to give me a feedback write a comment below