By Casey Liss
RxSwift Primer: Part 4

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? A Driver 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():

asDriver(onErrorJustReturn: Int)

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 to Driver
  • We can use map on a Driver and it will remain a Driver

It’s important to really let that second bullet sink in: most Rx operators such as map, filter, etc. all work on Drivers. Conveniently, they also return Drivers. 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 Drivers 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 with 0 and add 1 each time
    • Convert that to a Driver to ensure we never error out and are on the main thread
    • Convert the Int to a String
    • Push that value into the UILabel
  • 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 UITableViews, 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's text 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.


  1. “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 or disposed 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.