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.
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.
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:
|
|
|
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)
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