Split laps timer with RxSwift and RxCocoa: Part 2

In my post from last week I worked on creating a split lapse timer app (last week’s post). But later on when I was playing with the application I noticed that I naturally would like to have means to start or stop the timer.

Well this week I am implementing exactly this functionality.

The first thing I thought about was how to implement state in my app because a timer clearly has two distinct states either running or not running. That got me thinking about combining signals, mapping, you know, all the good stuff.

If you want to follow along you can download the starter project I prepared. It is in the shape where last weeks blog posts leaves off but I’ve added a couple of buttons in the user interface:

Download the starter project to follow along here: rx_laptimer_starter.zip

Now let’s put all those buttons to work!

My very first idea was to try generating a sequence of values to describe the current state of the timer. The start button would produce true values and the stop button will produce false values. When merged I will get one sequence that emits every time the state changes:

o---- (play tap) true--- (stop tap) false --- (play tap) true --->

So at the top of viewDidLoad() I created a new Observable like so:

let isRunning = [btnPlay.rx_tap.map({_ in true}), btnStop.rx_tap.map({_ in false})]
    .toObservable()
    .merge()
    .startWith(false)
    .shareReplayLatestWhileConnected()

First I mapped the taps on btnPlay to true and the taps on btnStop to false and merged them together.

The Observable starts with a false value to give the user the opportunity to start the timer at their convenience.

I printed the values the new observable emits and was quite happy with the result:

isRunning.subscribeNext({state in
    print(state)
}).addDisposableTo(bag)

The code printed true or false in the console each time I pressed play or stop. Neato!

Now it looked very easy to bind that Observable to the buttons’ rx_enabled property to actually make the UI reflect the timer state. I could as well hide the laps button when the timer isn’t running at all!

And since some of the controls needed to be enabled when the timer is running and others when it isn’t - I made myself yet another observable and bound the controls like so:

let isntRunning = isRunning.map({running in !running}).shareReplay(1)

isRunning.bindTo(btnStop.rx_enabled).addDisposableTo(bag)
isntRunning.bindTo(btnLap.rx_hidden).addDisposableTo(bag)
isntRunning.bindTo(btnPlay.rx_enabled).addDisposableTo(bag)

The stop button is enabled while the timer is running. Play is enabled when the timer is paused.

I really love this kind of code. No ifs no switches; once you get the code running it’s very difficult to mess up and there’s simply no space for introducing bugs. Everything is air-tight.

The app started with the laps button hidden and only the play button enabled like so:

Further you could click play just once because it instantly became disabled. How cool :]

Now my problem was that even though the UI already reflected the different states of the timer - well … the timer didn’t care at all about any of that :]

I looked into the RxSwift implementation of a timer but didn’t find a way how to pause it (I guess that it couldn’t implement state, who knows …). That’s why I thought I’d move away from directly binding the timer to the UI and implement my own counter.

At the time my timer looked like this:

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

Well, I thought, I just need to somehow combine isRunning and timer and filter the observable output when isRunning is false.

So I did the following: I appended to the existing timer an operator to combine it with the latest value from isRunning:

.withLatestFrom(isRunning, resultSelector: {_, running in running})

You see that I just ignored the value emitted by timer since I never use it for anything and return from withLatestFrom the unchanged running input parameter:

(Int, Boolean) -> withLatestFrom -> Boolean

Next I could simply use filter to stop the observable from emitting when the timer is not running:

.filter({running in running})

And last but not least I had to attach a counter, but that was already something I knew how to do with scan like so:

.scan(0, accumulator: {(acc, _) in
    return acc+1
})

What scan does above is to count how many times the timer fired while isRunning was true (which is exactly what I wanted).

Finally I had to set the initial value to show in the UI and to share the result between all observers:

 .startWith(0)
 .shareReplayLatestWhileConnected()

The complete code of the enhanced timer observable looked like this:

timer = Observable<NSInteger>.interval(0.1, scheduler: MainScheduler.instance)
    .withLatestFrom(isRunning, resultSelector: {_, running in running})
    .filter({running in running})
    .scan(0, accumulator: {(acc, _) in
        return acc+1
    })
    .startWith(0)
    .shareReplayLatestWhileConnected()

And that’s a wrap :] Now my timer app had a stateful UI and split lap all implemented without a single if!

You can get the completed project from here: rx_laptimer_finished.zip

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.