In my RxSwift primer series, we’ve:
Today, we’ll take this to the next step by leveraging a feature in RxCocoa.
Recap
When we left things, our ViewController
looked like this:
class ViewController: UIViewController {
// MARK: Outlets
@IBOutlet weak var label: UILabel!
@IBOutlet weak var button: UIButton!
// MARK: ivars
private let disposeBag = DisposeBag()
override func viewDidLoad() {
self.button.rx.tap
.debug("button tap")
.scan(0) { (priorValue, _) in
return priorValue + 1
}
.debug("after scan")
.map { currentCount in
return "You have tapped that button \(currentCount) times."
}
.debug("after map")
.subscribe(onNext: { [unowned self] newText in
self.label.text = newText
})
.addDisposableTo(disposeBag)
}
}
We’ve gotten rid of our stored state, leveraged the scan
function, and
used map
to make it clearer what each step of the process does. Today, we’re
going to introduce the Driver
object.
Driver
If you wanted to push the result of an observable chain onto a UI element, such
as the String
we’re generating above, that’s fraught with peril:
- What happens if that
Observable
errors? How does the UI element handle that? - What happens if the
Observable
is being processed on a background thread? Updating user interface elements from a background thread is a big no-no.
Enter the Driver
.
A Driver
is one of the “units” that is offered in RxCocoa. A Driver
, like
all the other units, is a special kind of Observable
. In the case of a
Driver
, it has the following qualities:
- It never errors
- It is always observed on the main thread
- It shares side effects
Of those, we’re going to focus on the first two.[1] It fixes both of our problems above:
- What happens if a
Driver
errors? It can’t. - What happens if a
Driver
is being processed on a background thread? ADriver
guarantees it will always be processed on the main thread.
Naturally, a Driver
solves our problems. Furthermore, a Driver
can drive
the value of a UI element. This special trick of a Driver
allows us to wire
a UIControl
to an Observable
’s output without any manual call to subscribe()
.
Using a Driver
To use a driver, we’re going to modify our ViewController
code a bit. We’ll
remove the subscribe()
call entirely, and use the Driver
to drive()
the
UILabel
’s text
property instead.
First, we have to create a Driver
. The general way to do this is to simply
convert an Observable
to a Driver
using the Observable
’s asDriver()
function. Notice the parameter we have to provide to asDriver()
, if we do
the conversion right after the scan()
:

Immediately, we hit something unexpected: we have to provide an Int
in order
to create the Driver
. The reason why is clear from the parameter name:
onErrorJustReturn
. To convert an Observable
to a Driver
, we need to provide
a value to use in case the source Observable
errors. In our case, we’ll just
use zero.
Here’s our new chain so far, before the call to subscribe()
:
self.button.rx.tap
.debug("button tap")
.scan(0) { (priorValue, _) in
return priorValue + 1
}
.debug("after scan")
.asDriver(onErrorJustReturn: 0)
.map { currentCount in
return "You have tapped that button \(currentCount) times."
}
.debug("after map")
A couple things should be noted here:
- We can sprinkle calls to
debug()
before and after the conversion toDriver
- We can use
map
on aDriver
and it will remain aDriver
It’s important to really let that second bullet sink in: most Rx operators such
as map
, filter
, etc. all work on Driver
s. Conveniently, they also return
Driver
s. Given that, it doesn’t usually matter where the conversion to a
Driver
happens in a chain. I could have done it before the scan
above, if I
preferred. Just remember everything that comes after will be on the main thread.
Regardless of where I place the asDriver()
, the result of the above chain is
Driver<String>
. Let’s leverage that to drive our UILabel
’s text
property.
We can do so using the Driver
’s drive()
function:
self.button.rx.tap
.debug("button tap")
.scan(0) { (priorValue, _) in
return priorValue + 1
}
.debug("after scan")
.asDriver(onErrorJustReturn: 0)
.map { currentCount in
return "You have tapped that button \(currentCount) times."
}
.debug("after map")
.drive(self.label.rx.text)
.addDisposableTo(disposeBag)
We’ve now removed our call to subscribe()
, and are simply asking the Driver
to push updates onto the UILabel
’s rx.text
property. We still need to add
this to a DisposeBag
, since there’s an implicit subscription made by the
Driver
. You don’t have to remember that, as there will be a warning if you
forget.
Like before, let’s run, tap the button once, and see what is left in the console. Here again, I’ll add newlines for clarity:
2016-12-17 15:27:55.934: after map -> subscribed
2016-12-17 15:27:55.935: after scan -> subscribed
2016-12-17 15:27:55.936: button tap -> subscribed
2016-12-17 15:27:58.303: button tap -> Event next(())
2016-12-17 15:27:58.304: after scan -> Event next(1)
2016-12-17 15:27:58.304: after map -> Event next(You have tapped that button 1 times.)
This actually looks just the same as it did before. The fact that we’re using a
Driver
is irrelevant for the purposes of debug()
as Driver
s are really just
a special kind of Observable
.
Cleanup
Now that we know everything is working as intended, let’s get rid of our calls
to debug()
. Here’s the final, Rx-ified version of ViewController
:
class ViewController: UIViewController {
// MARK: Outlets
@IBOutlet weak var label: UILabel!
@IBOutlet weak var button: UIButton!
// MARK: ivars
private let disposeBag = DisposeBag()
override func viewDidLoad() {
self.button.rx.tap
.scan(0) { (priorValue, _) in
return priorValue + 1
}
.asDriver(onErrorJustReturn: 0)
.map { currentCount in
return "You have tapped that button \(currentCount) times."
}
.drive(self.label.rx.text)
.addDisposableTo(disposeBag)
}
}
🎉
You can see this version of the code at Github. Look at how pretty it is! I’m being silly, but also somewhat serious. This new code has several advantages over what we started with:
- No stored state; all state is simply computed
- Less chance for bugs because there is no stored state to get out of whack with the user interface
- Dramatically improved local reasoning; it’s clear the steps we’re taking;
- Start with the
UIButton
tap scan
all occurrences; start with0
and add1
each time- Convert that to a
Driver
to ensure we never error out and are on the main thread - Convert the
Int
to aString
- Push that value into the
UILabel
- Start with the
- Furthermore, there’s no other methods involved, and no magical Interface Builder wiring
There is one disadvantage, however. This code is quite a bit longer than where we started:
class ViewController: UIViewController {
// MARK: Outlets
@IBOutlet weak var label: UILabel!
// MARK: ivars
private var count = 0
@IBAction private func onButtonTap(sender: UIControl) {
self.count += 1
self.label.text = "You have tapped that button \(count) times."
}
}
That’s unfortunate, but this is a really crummy example in that regard. I
chose this example because I didn’t want to get bogged down in irrelevant
details, such as UITableView
s, etc. This example of simply counting how
many times a button is tapped is way simpler than most uses of Rx.
The Canonical Example
Everyone’s favorite example of what makes Rx so great is handling a user entering a search phrase. In fact, I was part of a conversation with Brent Simmons and Jamie Pinkham about this back in April. There’s more discussion over at Brent’s site, where Brent contrasts a traditional way of writing this search handler with RxSwift. The ground rules were:
- Changes to the search text must be coalesced over a period of 0.3 seconds.
- When the search text changes, and the text has four or more characters, an http call is made, and the previous http call (if there is one) must be canceled.
- When the http call returns, the table is updated.
- And: there’s also a Refresh button that triggers an http call right away.
While there is a fair bit of supporting code that we had to write to make this happen in RxSwift, satisfying the above requirements was really easy. The meat of that effort is here:
let o: Observable<String> = textField.rx.text
.throttle(0.3)
.distinctUntilChanged()
.filter { query in return query.characters.count > 3 }
You can see in that one line of code:
- We’re triggering off a
UITextField
’stext
property - We’re throttling it so that we ignore changes that occur in less than 0.3 seconds
- We’re ignoring two successive duplicates
- We’re ignoring entries less than 3 characters
Boom. 💥 That is why RxSwift is so cool.
Next Time
There’s still more to be done, however. We’ve been bad developers, and haven’t been unit testing our code as we go along. In part 5 of the series, I’ll describe how to do unit tests in RxSwift. Much like RxSwift itself, unit testing it is both very unlike what we’re used to while also being extremely powerful.
UPDATED 29 December 2016 1:30 PM: Added clarifying remarks about the
placement of asDriver()
in an Observable
chain.
“Shares side effects” is Rx-speak for “every subscriber shares one common subscription”, in contrast with the usual behavior, where every subscriber gets its own subscription. That means that there will only ever be one
subscribe
ordisposed
event, even if there are multiple subscribers. If this is confusing, well, that’s why it’s a footnote. You asked. More on this in the docs. ↩