[译] 使用 RxSwift 和 RxCocoa 拆分计时器 (一)

1,277 阅读6分钟
原文链接: vsccw.com

本文翻译自:Split laps timer with RxSwift and RxCocoa
原文地址:rx-marin.com/post/rxswif…
作者:Marin Todorov


我正在浏览RxMarbles,完全被sample函数所困扰。流程图看起来很随机:
marble diagram
起初我想 - “嘿,第二个序列正在被完全忽略!”,但是在我阅读之后,我发现:

第一个序列的元素是由样品发出的,而第二个序列的元素决定样品发出的时间,所以在某种程度上是-实际上A,B,C,D确实被完全忽略。

当我明白了sample做了些什么,我开始想知道这个功能是否有实际的用处:]
这让我想创建一个拆分计时器(注: split lap timer)应用程序来测试sample可以为我们做什么。在完成的项目中,我有一个定时器发出时间值(也就是一个序列),同时我也想要抓住(注: grap)或者sample每一次用户点击(是另一个序列)按钮的反应值。
下面是应用程序设置的流程图:
marble diagram when app setup
这是完成后的应用程序的样子:
finished
让我们开始构建这个应用吧:]
下面是我想要的拆分计时器应用的运行与功能: 1. 在程序启动时就启动计时器
2. 以 MM:SS:MS 的形式显示运行时间
3. 当用户点击了按钮Split Lap的时候添加一条拆分时间值
4. 以一个列表的形式展示拆分时间
5. 以列表头的形式展示拆分时间数目总和

1. 启动计时器

像我在前一篇文章中关于手动处理包内容那样,我添加一个timer

var timer: Observable<Int>!  

同时在viewDidLoad中让它每1/10秒运行一次。(我选择只显示一位数毫秒,所以不需要频率更高:])

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

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

这会使计时器运行,并填满控制台:

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

酷,这很好:](你一定会想,“好吧,我已经知道如何做这个了”)

2. 显示当前已过去时间

这也是我已经知道如何去做的部分。首先我写了一个小的函数,该函数会将已过去的时间格式化成想要的字符串。

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

然后回到viewDidLoad,我使用该函数绑定计时器到通过Interface Builder创建的Label中。

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

我真的很喜欢代码运行的步骤,下面是代码中一步步发生的事情:

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

函数式的代码非常棒,因为我已经取得了两个巨大胜利,我可以很容易重用stringFromTimeInterval,然后我也可以写一些非常简单的测试代码。
与此同时,定时器label已经成功显示了当前运行的时间:
当前运行时间

3. 当用户点击了Split Lap按钮的时候,抓取当前拆分的时间

在这里我应该可以达到我的终极胜利:使用sample。前几次尝试并没有让我迈出大步,直到我意识到rx.tap也是一个Observable

Duh,一切都是Observable

现在只剩下timer调用sample这一个问题了,并提供作为控制序列的按钮的属性rx.tap,就像这样:timer.sample(btnLap.rx.tap),怎么去做?(Whaaat?)
现在我每次点击按钮sample都会发出timer产生的最新值。由于我对数字不感兴趣,但是在格式化字符串的时候我再次将结果使用stringFromTimeInterval转换。
因为我需要创建一个分离的时间列表,我使用scan。实际上我第一次是想用reduce,因为我想在列表中累加值,但是后来意识到,我需要产生一个序列,同时这个序列发出每个新值得列表。因此我意识到了我得使用scan

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

所以,每次sample发出新的拆分时间时,scan会扫描到目前为止所有拆分时间的数组。
不知道如何解释scan更简单,但是我会尝试:在RxSwift中的任何时候,如果你正想使用reduce,那么你也可以使用scan代替:]

4. 显示到目前为止所有的拆分时间表

OK, 所以我得到了lapsSequence发出的拆分时间数组。在参考了RxExample之后,我们就可以操作UITableView了。

//show laps in table
lapsSequence  
        .bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
        cell.textLabel?.text = "\(row + 1)) \(element)"
        }
        .disposed(by: bag)
.addDisposableTo(bag)

同时我的APP已经正常工作了。
Working
我每一次点击Split Lap按钮,都会添加一条拆分时间到TableView上。Sweet~

5. 在TableView标题上显示拆分时间总数

这部分是让我最失望的。因为没有一个绑定我可以用在TableView的Header上,同时我又不想添加数据源,这样会使代码复杂化。
于是我突然想到在我的控制器中添加一个Label,并将它作为TableView的header,然后将拆分次数总数绑定到这个label的rx.text上。
所以我添加一个label到视图控制器上:

let tableHeaderView = UILabel()  

然后是一个扩展,并将该label设置为TableView的header:

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

我知道如何设置TableView的代理到ViewController这个类中(让我们回到ViewDidload中):

//set table delegate
tableView  
  .rx.setDelegate(self)
  .addDisposableTo(bag)

现在又出现了变况,我不得不将lapsSequence从数组映射到单个字符上(例如:5次),并将该字符串绑定到TableView header上。 我过度兴奋地想使用scan,但是代码确实有些不优雅。后来在RxSwift slak问了KrunoslavZaher, 他启发了我,因为我有一个数组,这样我就可以使用map将其转换成字符串。 下面是最终在viewDidLoad中的代码:

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

因为lapsSequence发出的是所有拆分时间的数组,每次发出一个新的拆分时间,我只取该数组,并返回一个包含元素数量的字符串。 此外,我将初始值设置为no laps, 这样很好。我将所有内容直接绑定到tableHeaderView.rx_text上。
这是完整的应用程序!

您可以下载完成的项目,并在这里尝试:rx_laptimer.zip.
希望这篇文章是有帮助的,如果你想联系你,你可以找到我在这里Twitter

注: 原文提供的代码下载地址还不是Swift 3的,所以我在这里已经更改了原作者提供的地址。代码经过Xcode8.2.1测试,完全可以测试通过。
对于英语好的同学,推荐阅读作者的英文原文。