Split laps timer with RxSwift and RxCocoa

I was browsing through RxMarbles and was totally baffled by the sample function. The marble diagram looks pretty random at first sight:

At first I thought - “Hey, that second sequence is getting totally ignored!”. But after I read the description I figured it out:

The first sequence’s elements is what sample emits, while the second sequence’s elements determine when sample emits. So in a way yes - the actual values A, B, C, D do get totally ignored.

When it was clear to me what sample does I started wondering if this function has any practical application :]

This brought me to creating a split lap timer app to test what sample can do for me. In the finished project I have a timer emitting time values (aka the first sequence) and I want to grab (or sample) the values whenever the user taps a button (aka the second sequence).

Here’s how the marble diagram looks like for the app setup:

And this is how the app looks like when finished:

Let’s build that app :]

Here are the specs I wanted for my split lap timer app:

  1. start the timer at launch
  2. show the running time in format MM:SS.MS
  3. when the user taps “Split Lap” add a split time
  4. show a table of the split times
  5. show a table head with the total of laps

1 Start a timer

Like in my previous post about manually disposing bag’s contents I added a timer:

var timer: Observable<NSInteger>!

And in viewDidLoad let it run every 1/10 of a second (I chose to show only 1 digit for milliseconds so no need to fire more often):

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

timer.subscribeNext({ msecs -> Void in
  print("\(msecs)00ms")
}).addDisposableTo(bag)

This got the timer running and filling up the console:

000ms
100ms
200ms
300ms
400ms
500ms
600ms
700ms
800ms
900ms
1000ms
1100ms

Cool - that was easy (well, I already knew how to do that part, lol)

2 Show the current elapsed time

This was also a part I already knew how to do. First I put together a little function to take the elapsed time and return a nicely formatted string:

func stringFromTimeInterval(ms: NSInteger) -> String {
  return String(format: "%0.2d:%0.2d.%0.1d",
    arguments: [(ms / 600) % 600, (ms % 600 ) / 10, ms % 10])
}

And then back in viewDidLoad I used it to bind the timer to a label I added via Interface Builder:

//wire the chrono
timer.map(stringFromTimeInterval)
  .bindTo(lblChrono.rx_text)
  .addDisposableTo(bag)

I really love how the code flows and tells the story of what should happen:

timer -> 1,2,3 -> stringFromTimeInterval -> "string", "string" -> lblChrono

Functional code is awesome because I get 2 huge wins for free: I can easily reuse stringFromTimeInterval and I can write very simple tests for it.

At this point the timer label already displayed the elapsed time:

3 Grab the split time when the user taps the “Split Lap” button

Ok here I was supposed to have my ultimate win by using sample. The first few tries didn’t get me far until I realized that the rx_tap property on UIButton is also an Observable.

Duh, everything is an Observable :]

Then it was just a matter of calling sample on my timer and providing as a control sequence the rx_tap property of the button like so: timer.sample(btnLap.rx_tap) Whaaaat?

Now each time I tapped the button sample emitted the latest value produced by timer. And since I wasn’t interested in the number but in the formatted string I again mapped the result with stringFromTimeInterval.

And since I needed to build a list of those split times I used scan. Actually at first I came around reduce because I was thinking of accumulating values in a list, but then realized I needed to produce a sequence that emits the list for each new value… hence I kind of knew I got to use scan:

let lapsSequence = timer.sample(btnLap.rx_tap)
    .map(stringFromTimeInterval)
    .scan([String](), accumulator: {lapTimes, newTime in
        return lapTimes + [newTime]
    })
    .shareReplay(1)

So - each time sample emits a new lap time scan emits an array of all the split times so far.

Not sure how to explain scan more simply but I’ll try: In RxSwift any time you’re thinking of using reduce chances are you need scan instead :]

4 Show a table of the split times so far

Ok so I got lapsSequence emit an array of split times. From there (after consulting RxExample) was a walk in the park to wire up the table view:

//show laps in table
lapsSequence.bindTo(tableView.rx_itemsWithCellIdentifier("Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
    cell.textLabel!.text = "\(row+1)) \(element)"
}
.addDisposableTo(bag)

And my app was already working!

Each time I tap the “Split Lap” button I get a new split time added in the table view. Sweet!

5 Show a table header with the number of laps

This part was the one that tripped me the most. There wasn’t a binding I could use for the table header and I didn’t want to complicate the code unneccessarily by adding a section table data source.

What came to mind was to add a UILabel property to my view controller and use it as the table header view. Then bind the count of split times to the rx_text of that label.

So I added to the view controller class:

let tableHeaderView = UILabel()

And then an extension to set this label as my table view header:

extension ViewController: UITableViewDelegate {
  func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    return tableHeaderView
  }
}

I knew how to set my class as the proxy delegate to the table view (back in viewDidLoad):

//set table delegate
tableView
  .rx_setDelegate(self)
  .addDisposableTo(bag)

And now came the coup d’etat - I had to map lapsSequence from an array to a single string (e.g. “5 laps”) and bind that string to the table header.

I got overexcited about using scan but the code did feel itchy so after asking around on the RxSwift slack KrunoslavZaher enlightened me that since I have one array I can turn it into one string by simply using map.

Here’s the final code addition to viewDidLoad:

//update the table header
lapsSequence.map({ laps -> String in
    return "\t\(laps.count) laps"
})
.startWith("\tno laps")
.bindTo(tableHeaderView.rx_text)
.addDisposableTo(bag)

Since lapsSequence emits an array of all split times each time a new split time is emitted I just take that array and return a string with the number of elements.

Additionally I set the initial value to “no laps” and that’s pretty much it - I bind everything directly to tableHeaderView.rx_text.

And that’s the complete working app!

You can download the completed project and give it a try here: rx_laptimer.zip.

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.