Implementing state with scan in RxSwift

Intro

Common misconception is that you cannot have state with Rx. Well you can - and there’s a special operator that helps you to: scan.

If you’ve ever used reduce in Swift - scan is a very similar operator but reduce goes over the complete sequence and gives you the final value of the accumulator while scan emits each intermediate value as well.

If you haven’t used reduce - no worries you’ll get to understand scan from the examples below. Let’s get started!

Creating a boolean switch

When you have to deal with UI you inevitably have to deal with state. Imagine a button that toggles between selected and deselected state as the user taps it repeatedly. One tap - the button is selected, another tap - it’s not, a third taps selects it again, etc.

UIButton.rx_tap always emits the same value of Void so it doesn’t provide you with any information to decide whether to select or deselect the button. Enter scan

scan takes two parameters:

  • initial value - you can think of it as the first value of your state
  • closure(lastState, newValue) - scan runs that closure each time it gets a new value - it calls it with two parameters: the last state you had and the value that was just emitted.

You might wander what the state is? Anything you want it to be (I know… clich├ęs) - it can be a Bool, String, an Array anything you need for your code. Let’s look at the first example to make everything click together.

I had to make button get selected/deselected as described earlier so I used scan in the following way:

myButton.rx_tap.scan(false) { lastState, newValue in
    return !lastState
}
.subscribeNext {value in
    print("tap: \(value)")
}

First of all consider how the scan above transforms the data stream. It starts with Void values emitted by rx_tap but then scan maps those to Bool values (determined by the type of its return type):

(tap) —> Void —> (scan) —> Bool —> (subscribeNext)

So scan starts with a false state and on each button tap it applies the closure. The first time lastState = false and newValue = Void (actually newValue is always Void so I’ll ignore till the end of this example). You return the negation of lastState, which is true.

The second time around lastState is true because this is what you return the first time. And your return false.

Third time around lastState is false and you return true. Etc. etc. etc.

The console output is:

tap: true
tap: false
tap: true
tap: false
tap: true

So as you can see you can implement state but it’s contained within the closure you supply to scan. After scan you get a data stream of the type your closure returns - that’s all :)

So to complete the select/deselect example you just need to bind the scan result to your button’s rx_selected sink like so:

myButton.rx_tap.scan(false) { lastState, newValue in
    return !lastState
}
.bindTo(myButton.rx_selected)

Creating a counter

Now let’s get beyond alternating between Bool values and write a code to count how many times a button has been tapped.

I actually had to do this few times in the last couple months so I’ll just put in here the code:

myButton.rx_tap.scan(0) { lastCount, newValue in
    return lastCount + 1
}
.subscribeNext {value in
    print("taps: \(value)")
}

This time around the state is of type Int and it starts with 0. Each time the user taps the button scan returns the last count plus 1. As the user taps the button the Console shows:

taps: 1
taps: 2
taps: 3
taps: 4
taps: 5

Once you grasp how the last state thing works it’s pretty easy isn’t it? :)

Geting the last N values

Somebody in the RxSwift Slack asked for this and it’s an interesting (but very simple to solve) example.

How to get the last N elements from an Observable?

For example if you have a sequence of Int values: [0, 1, 2, 3, 4, 5, 6] how to have the last 3 each time a new value is emitted?

Well, this smells like having a state since you need to “remember” values. So it must be solvable with scan.

Obviously the first time the sequence emits you don’t have any previous values so the initial state to give to scan is an empty array []. Let’s have a look at the complete code:

let numbers = [0, 1, 2 , 3, 4, 5, 6].toObservable()

numbers.scan([]) { lastSlice, newValue in
    return Array(lastSlice + [newValue]).suffix(3)
}
.subscribeNext {value in
    print("last 3: \(value)")
}

Each time scan adds the emitted value to the last array you had and than chops 3 elements off the end. Now the data stream looks like so:

(numbers) —> Int —> (scan) —> [Int] —> subscribeNext

And the Console output:

last 3: [0]
last 3: [0, 1]
last 3: [0, 1, 2]
last 3: [1, 2, 3]
last 3: [2, 3, 4]
last 3: [3, 4, 5]
last 3: [4, 5, 6]

But didn’t I say I wanted elements of three? Just filter the output of scan and check for the length of the emitted array and that’s it ;)

Using enums for state

I didn’t have to do any more advanced scan stuff yet but I can imagine all kinds of uses for it. Let’s say you’re building a space game.

When you start a level in your space game your ship has to make it through an asteroid field. Therefore the longer the ship is “alive” the more points you get, etc.

So you can build a timer that tracks the level time and bind the level state and the amount of points earned like so:

enum LevelState {
    case Normal, PowerUp
}

let timer = Observable<NSInteger>.interval(0.25, scheduler: MainScheduler.instance)

timer.scan((LevelState.Normal, 0)) { lastState, _ in
    
    switch lastState.0 {
    case .Normal:
        if lastState.1 > 1000 {
            return (.PowerUp, lastState.1 + 30)
        } else {
            return (.Normal, lastState.1 + 30)
        }
    case .PowerUp:
        return (.PowerUp, lastState.1 + 100)
    }
}
.subscribeNext {_, points in
    print("pts: \(points)")
}

You have two states in your level - normal mode and power-up. Once the player manages to survive past 1,000 points they start gaining more points much faster. To do that you have two states listed in an enum LevelState.

The state is a tuple of type (LevelState, Int) - the first element tracks the current level state, and second is the points counter.

And this is still a pretty simple example of what is possible with scan and few lines of code. Woot!

Look those numbers fly once you’re past the 1,000 mark!

pts: 810
pts: 840
pts: 870
pts: 900
pts: 930
pts: 960
pts: 990
pts: 1020
pts: 1050
pts: 1150
pts: 1250
pts: 1350

Conclusion

scan is simply fantastic and as I wrote in an earlier post - any time you’re tempted to use reduce you probably need scan.

I hope that post has been useful and if you have any operator that bums you out or can’t figure out ping me on Twitter - I’m also still learning but have few of those figured out pretty well :)

Do you know a better way to do any of this? Seen a bug? Ping me on Twitter.

Hope that post was helpful, and if you want to get in touch you can find me here

Share this post:


If you'd like to learn how to create professional production apps with RxSwift, the best resource out there is the RxSwift book written by Florent Pillet, Junior Bontognali, Marin Todorov, & Scott Gardner.

It features 20+ chapters covering the basics, the Rx operators, and advanced topics like testing, error handling, and app architecture.

Available from Ray Wenderlich: » Learn more.
Tags// , , ,