By Casey Liss
RxSwift Primer: Part 3

So far we’ve:

Today, we’re going to start really leveraging Rx for what it’s best at: eliminating stored state, and thus, preventing avenues for bugs.

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()
    private var count = 0
    
    override func viewDidLoad() {
        self.button.rx.tap
            .debug("button tap")
            .subscribe(onNext: { [unowned self] _ in
                self.onButtonTap()
            }).addDisposableTo(disposeBag)
    }
    
    @IBAction private func onButtonTap() {
        self.count += 1
        self.label.text = "You have tapped that button \(count) times."
    }
}

This is fine, but it’s already obvious that something is redundant.

Quick Win

As a first step, let’s get rid of the useless onButtonTap(). We can just fold that into the subscribe():

class ViewController: UIViewController {

    // MARK: Outlets
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var button: UIButton!
    
    // MARK: ivars
    private let disposeBag = DisposeBag()
    private var count = 0
    
    override func viewDidLoad() {
        self.button.rx.tap
            .debug("button tap")
            .subscribe(onNext: { [unowned self] _ in
                self.count += 1
                self.label.text = "You tapped that button \(self.count) times."
            }).addDisposableTo(disposeBag)
    }
}

This is definitely an improvement, but there’s still many more things that we can do in order to make things better.

Scan

In the first part of this series, I said:

Rx brings a bunch of tools to the table; some are the ones we already know. Some are totally new. Once you get to know the tools, it’s easy to reason about what is being done to a stream.

Today we’re going to add a new tool to our toolbox: scan. The scan function is summarized on the official Rx site as:

Apply a function to each item emitted by an Observable, sequentially, and emit each successive value.

What?

In the documentation, there’s an interesting tidbit that sounds vaguely familiar:

This sort of operator is sometimes called an “accumulator” in other contexts.

This is still confusing though. Let’s see if we can make sense of this by checking out the marble diagram:

Scan marble diagram

Now this “accumulator” seems to make sense. With each new input (1, 2, 3, 4, 5), we seem to be adding that value to the sum of all previous values.

  • 1 + 0 = 1
  • 2 + 1 = 3
  • 3 + 3 = 6
  • 4 + 6 = 10
  • 5 + 10 = 15

Suddenly accumulator makes more sense. In fact, this looks very similar to vanilla Swift’s reduce() function.

Using Scan

So, how do we actually use scan? Looking at the way it’s declared gives us a hint:

public func scan<A>(_ seed: A, accumulator: @escaping (A, Self.E) throws -> A) -> RxSwift.Observable<A>

It looks like we provide a seed value, and then a closure to perform whatever accumulation we’d like. What if we seed the scan with 0? Rather than overwriting what we have, let’s do this separately:

let o = self.button.rx.tap
    .scan(0) { (priorValue, _) in
        return priorValue + 1
    }

Now, using Xcode’s quick info popover (⌥-Click), we can see what type o is:

RxSwift scan type info

Awesome! We’ve now got an Observable<Int>, which theoretically means we have an Observable that will signal the current tap count every time the button is tapped. Perfect!

Aside: I find it’s often useful to break one Observable chain up as we’ve done above, and leverage Swift’s type inference to ensure that I’m operating on the types I expect. In this contrived example, it’s pretty obvious what’s happening, but in more complex cases—especially when we begin combining streams—things get hairy quickly.

Additionally, the Swift compiler’s type inference often gets tripped up once you get to combining many different streams and mutating them. One easy way to cheat and get back into the compiler’s good graces is to split up these calls, and if necessary, annotate the types as required.

Wiring Up

Let’s dump our temporary variable o and use this scan. Further, let’s also add a couple debug()s for good measure. Our new chain now looks like this:

self.button.rx.tap
    .debug("button tap")
    .scan(0) { (priorValue, _) in
        return priorValue + 1
    }
    .debug("after scan")
    .subscribe(onNext: { [unowned self] currentCount in
        self.label.text = "You have tapped that button \(currentCount) times."
    })
    .addDisposableTo(disposeBag)

Notice something important here: we have two calls to debug():

  • Our existing one that hangs off of button.rx.tap
  • A new one that hangs off of the scan()

Let’s run the app, tap three times, then look at both the screen and output. I’d drop an animated GIF of the screen behavior, but it looks the same as it always did, which is a victory. Looking at the console output, I’ve added some newlines for clarity, but otherwise haven’t changed anything:

2016-12-16 21:55:12.661: after scan -> subscribed
2016-12-16 21:55:12.662: button tap -> subscribed

2016-12-16 21:55:15.807: button tap -> Event next(())
2016-12-16 21:55:15.807: after scan -> Event next(1)

2016-12-16 21:55:16.728: button tap -> Event next(())
2016-12-16 21:55:16.728: after scan -> Event next(2)

2016-12-16 21:55:17.628: button tap -> Event next(())
2016-12-16 21:55:17.628: after scan -> Event next(3)

Notice that not only are there two entries per event on the button.rx.tap Observable, one for each debug() call, but they are showing different "payloads". The first is the ()/Void we discussed last time. The second is the Int that comes out of scan. Pretty neat.

It’s important to remember that debug() shows you the state of the stream at that point in the composition. Sprinkling debug() calls throughout one composition, as we have done above, can tell you loads of information about your transformations.

Dropping State

Did you notice what else is different about the above? Before our scan, the subscribe() closure looked like this:

self.count += 1
self.label.text = "You tapped that button \(self.count) times."

After scan:

self.label.text = "You have tapped that button \(currentCount) times."

We’ve dropped our dependency on the count instance variable. In turn, this means we have no stored state in this class anymore. We can now safely remove count from the class entirely.

Now, Rx is starting to spread its wings: we have removed stored state, and as such, we’ve removed a potential avenue for bugs. Awesome! 🎉

You can see this version of the code at this commit.

Small Improvement

Though not strictly necessary, let’s demonstrate how we can split things out slightly. Sometimes it helps to split a compound operation into multiple individual operations for code clarity. Remember: clever code is hard to maintain code.

In our case, let’s leverage map to split out the generation of the message that is being sent to the UILabel:

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)

In this more verbose version of our Observable chain, it’s abundantly clear what is happening:

  • We’re reacting to events on a UIButton's tap stream
  • We’re scanning it to reduce it to a single Int
  • We’re mapping that Int and creating a String from it
  • We’re subscribe()ing to that Observable<String> and setting that String on a UILabel.

Yes, it’s quite a bit longer than the procedural version we started with. That’s a bummer, but in a contrived example like this one, it’s to be expected. Most systems using Rx are not this simple, and have far more wild things going on. Thus, in more complex examples of Observable streams, the savings are often considerable.

Regardless of how long it is, I’d argue this is actually an extremely approachable block of code. As I said above, it’s eminently clear what is happening. It’s clear what the input is, and how we’re mutating it to get to our final subscribe(). It’s so much easier to reason how this code works, since there’s no reliance on other functions, and no reliance on Interface Builder wiring.

Since we examined the console output from the scan, let’s take a look at how it looks with our newly introduced map. This time, I’ll only tap the button once, and I will continue to add newlines for clarity:

2016-12-16 22:13:41.517: after map -> subscribed
2016-12-16 22:13:41.518: after scan -> subscribed
2016-12-16 22:13:41.519: button tap -> subscribed

2016-12-16 22:13:45.766: button tap -> Event next(())
2016-12-16 22:13:45.766: after scan -> Event next(1)
2016-12-16 22:13:45.768: after map -> Event next(Optional("You have tapped that button 1 times."))

As exepected, through the various debug() calls, we can trace our button tap from ()1You have tapped the button 1 times. Very neat.

You can see this version of the demo app at this commit on Github.

Next Steps

We’ve now had a big win and removed all stored state in our ViewController. Next time, we’ll take things a little bit further, and leverage one of the higher order classes that RxCocoa makes available to us: the Driver. We’ll also clean things up a bit, and then take stock of where we’ve landed.