[翻译]RxSwift入门(3)

标题:RxSwift Primer: Part 3 ,作者 Casey Liss,2016-12-19
原文:https://www.caseyliss.com/2016/12/19/rxswift-primer-part-3

没取得版权,盗版翻译一下,英文水平烂,不逐字逐句翻译了,别在意细节

迄今为止,我们已经:

今天,我们将会开始真正的利用Rx来做它最擅长的事:消除储存状态,由此来预防bug。

回顾

上次我们的ViewController代码是这样的:

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."
}
}

代码可以跑,但是已经很明显有冗余的味道了。

Quick Win(这标题怎么翻译?)

作为第一步,让我们摆脱无用的onButtonTap()方法,我们可以把方法里面的代码展开到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)
}
}

这绝对是大进一步,但依然有很多地方可以优化。

Scan

在第一篇里我说过:

Rx带来了一堆工具,有些我们已经见过了(高阶函数map),有些是全新的(如CombineLatest)。一旦你了解了这些工具,你就可以很容易地理解被怎样处理了

今天我们来介绍一个新的工具:scanscan函数在Rx官网的总结是:

对Observable发射的每一项(值)都应用一个函数,然后按顺序再发射出每一个值。

没听明白?

在文档里,有句有趣而又让人觉得莫名熟悉的话:

这种运算符在其他地方有时候会被称为“累加器”

还是觉得有点含糊的话,我们可以通过弹珠图来理清:



现在这个“累加器”看上去很清楚了。以(1, 2, 3, 4, 5)里的每个值作为输入,每个值都加上前一个,累加起来。

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

豁然开朗。事实上,这种行为看上去与Swift原生的reduce()函数非常像。

使用 Scan

那实际上我们怎样去用scan呢?从它的函数声明可以看出点眉目:

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

看上去像是让我们提供一个种子值(seed value,可以理解为初始的值),然后闭包就会按预期进行累加。如果我们把scanseed设置为0会怎样呢?为了不搞乱我们已经写好的代码,我们在另外一个地方写些测试代码:

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

现在对着变量o按Option+鼠标左键,我们可以看到o的类型:

哇塞!我们得到的是一个Observable<Int>,理论上我们拿到了一个可以发射信号的Observable,它会把我们点击button的次数发射出来,完美!

旁注:我发现把一个Observable从链中独立出来通常是很好用的,利用Swift的自动类型推导,可以确保我没搞错操作的类型。假如你不这样做,在上面这个有点刻意为之的例子上,可能还不太容易感觉到好处,但是在更复杂的情况下,特别是当我们开始组合各种流的时候,你就会知道这么做的好处了。
还有,一旦你组合很多不同类型的流并且改变他们的时候,Swift编译器的类型推导经常抽风。一个简单的解决方案是,把函数调用都分割开,如有必要,自己写出变量类型,不要依赖它自行推导。

穿针引线

让我们干掉临时变量o然后用上scan。顺便还加些debug()来方便观察。新的代码是这样的:

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)

这里有几点值得注意——我们有两个debug()

  • 一个在button.rx.tap后面
  • 一个在scan()后面

现在运行起来,点击button三次,然后留意屏幕和控制台的输出。屏幕gif截图我就不录了,直接看控制台输出吧。(我加了些空行)

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)

注意到事件(event)有三组,每组有两个,分别对应上述的两个debug(),他们附带了不同的东西。第一个事件附带的是()/Void,这个我们上一篇讨论过了。第二个事件附带的是Int,由scan产生的。

要谨记debug()是把流在构成后的那个时间点的状态显示给你看。把debug()调用放在流构成的地方,可以把关于流的变化信息告诉你。

去掉状态

不知道你有没有留意到,我们没用scan之前,subscribe()里的闭包代码是这样的:

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

用了scan之后:

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

我们已经摆脱了对实例变量count的依赖,换言之,我们这个类再也没有状态储存了。我们现在可以把count从类中安全地移除了。

现在,Rx开始展现它的威力了:我们移除了储存状态,从源头上减少了bug的产生,棒棒哒!🎉

小改进

尽管不是必须的,但是有时候把一个复合的操作分割成多个各自独立的小操作可以让代码更清晰。谨记:聪明的代码是很难维护的。

还是我们的例子,我们利用mapUILabel要显示的文本分割开:

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)

在这个更详尽的Observable链中,发生的一切都很清晰:

  • 响应UIButton的点击流产生的事件
  • 通过scan把这些流归纳成单个Int
  • map把这个Int转换成String
  • 订阅(subscribe())上面产生的Observable<String>,把它的String设置到UILabel

没错,是稍微比我们前面写的版本长一点。这不太好,但是作为一个示例,完全正常。

不管它有多长,我还是认为它是极其容易理解的代码块。它清晰地表明了输入的是什么,然后我们怎样转化它到我们最终的订阅上。既不依赖其他函数,也不依赖Interface Builder连线。

我们前面检查过scan的控制台输出了,现在来看看加入map之后的输出。我只点击了button一次(依然加了些空行):

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."))

不出所料,通过这些debug()调用,我们可以跟踪button的点击从()->1->You have tapped the button 1 times.的转化过程,干净利落。

下一步

我们现在已经前进了一大步了——从ViewController里移除了所有储存状态,下一篇,我们将会更进一步,学会使用RxCocoa提供给我们的高阶类型:Driver.