[翻译]RxSwift入门(4)

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

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

迄今为止,我们已经:

今天我们将利用RxCocoa的特性来进行下一步改进。

回顾

上次的代码如下:

class ViewController: UIViewController {

// MARK: Outlets
@IBOutlet weak var label: UILabel!
@IBOutlet weak var button: UIButton!

// MARK: ivars
private let disposeBag = DisposeBag()

override func viewDidLoad() {
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)
}

}

我们利用scan函数摆脱了储存状态,还用map来让这条链得每一步处理过程更清晰。今天,我们准备介绍Driver对象。

Driver

如果你想把一个observable链的结果推到一个UI组件上,就像我们上面的String那样,会有几个坑要注意:

  • 如果Observable产生了error会怎样?UI元素应如何处理?
  • 如果Observable是在后台线程处理的话?在后台线程刷新用户界面肯定是作死了。

Driver

DriverRxCocoa提供的其中一个工具。一个Driver,也是一个特殊的Observable,它有这些特质:

  • 永远不会产生error
  • 总是在主线程处理
  • 共享副作用(side effects,需要参见shareReplay

我们暂时把注意力集中在前两点,它完全填了我们上面提到的坑。而且,Driver还可以把值推(drive)到UI组件上。这个特性可以让我们把一个UIControl和一个Observable的输出连起来,根本不需要手动对Observable进行subscribe()

使用Driver

使用driver之前我们得修改一下ViewController的代码,我们会去掉subscribe(),然后用Driverdrive()UILabeltext属性。

首先,我们要创建一个Driver,最简单的方法是调用ObservableasDriver()方法就可以把一个Observable转成Driver了。
要注意asDriver()要求传一个参数,如果我们直接在scan()后面加:



碰到些意外了:为了创建Driver,我们必须提供一个Int。从参数名里我们可以很清晰地看到原因:onErrorJustReturn。要把一个Observable转换成Driver,我们需要提供一个值,当被转换的这个Observable产生error的时候,会用这个值代替error。就我们的例子来说,只要0就好了。

现在,在subscribe()调用之前,我们新的链是这样的:

self.button.rx.tap
.debug("button tap")
.scan(0) { (priorValue, _) in
return priorValue + 1
}
.debug("after scan")
.asDriver(onErrorJustReturn: 0)
.map { currentCount in
return "You have tapped that button \(currentCount) times."
}
.debug("after map")

有些地方要注意:

  • 我们加了一些debug()在转换成Driver之前和转换之后
  • 对一个Drivermap,返回的还是Driver

第二点是特别重要的:Rx的很多操作符如mapfilter等,全部可以用在Driver,而且这些操作符在操作完之后都会返回Driver。鉴于此,通常在一个链的什么位置转换成Driver并不要紧。只要我愿意,可以在scan之前就转换成Driver。只要记住链在转换成Driver之后,后续的处理都会在主线程进行。

不论我把asDriver()放在什么位置,上面这个链的结果总是Driver<String>。利用转换后的Driver去推到UILabeltext属性上,我们用drive()函数就行了:

self.button.rx.tap
.debug("button tap")
.scan(0) { (priorValue, _) in
return priorValue + 1
}
.debug("after scan")
.asDriver(onErrorJustReturn: 0)
.map { currentCount in
return "You have tapped that button \(currentCount) times."
}
.debug("after map")
.drive(self.label.rx.text)
.addDisposableTo(disposeBag)

我们现在已经移除了subscribe(),用Driver来把新值推到UILabeltext属性。我们最后还是要把它添加到一个DisposeBag,因为其实Driver是在内部帮我们订阅了而已。你也不用死记硬背,你忘记了这步的话,编译器会用警告提醒你的。

跑起来,点击button一次,然后看控制台输出(加了些空行):

2016-12-17 15:27:55.934: after map -> subscribed
2016-12-17 15:27:55.935: after scan -> subscribed
2016-12-17 15:27:55.936: button tap -> subscribed

2016-12-17 15:27:58.303: button tap -> Event next(())
2016-12-17 15:27:58.304: after scan -> Event next(1)
2016-12-17 15:27:58.304: after map -> Event next(You have tapped that button 1 times.)

实际上和之前第三篇最后的输出一毛一样。对于debug()来说,转不转换成Driver对它根本无关痛痒,因为Driver也只是一种稍微特殊的Observable而已。

清理

既然一切都如按预期进行,是时候把debug()去掉了。下面是完全Rx版本的代码:

class ViewController: UIViewController {

// MARK: Outlets
@IBOutlet weak var label: UILabel!
@IBOutlet weak var button: UIButton!

// MARK: ivars
private let disposeBag = DisposeBag()

override func viewDidLoad() {
self.button.rx.tap
.scan(0) { (priorValue, _) in
return priorValue + 1
}
.asDriver(onErrorJustReturn: 0)
.map { currentCount in
return "You have tapped that button \(currentCount) times."
}
.drive(self.label.rx.text)
.addDisposableTo(disposeBag)
}

}

多么优雅啊!相比起我们最开始第一篇里写的代码,有如下一些优势:

  • 没有储存状态,所有状态都是通过简单计算得来的
  • 没有储存状态和用户界面同步,就更少机会引起bug
  • 极大改进了代码条理,每一步都很清晰:
    • UIButton的点击开始(返回Observable<Void>
    • scan所有发生的点击,从0开始,每次触发都递增1(返回Observable<Int>
    • 转换成Driver,确保不会产生error传给下面,确保在主线程操作(返回Driver<Int>
    • 转换IntString
    • 把这个String推到UILabel
  • 不涉及其他方法调用,没有魔法般的Interface Builder连线

缺点还是有的——代码比我们非Rx的版本稍长一些:

class ViewController: UIViewController {

// MARK: Outlets
@IBOutlet weak var label: UILabel!

// MARK: ivars
private var count = 0

@IBAction private func onButtonTap(sender: UIControl) {
self.count += 1
self.label.text = "You have tapped that button \(count) times."
}
}

很遗憾,但这只是因为我的demo比较糟糕。我举的例子比较糟糕是因为我希望尽可能用最简单的代码来给你演示Rx,免得你困扰于其他不重要的细节,不然我完全可以给你来一个UITableView什么的做例子,但是那就违背了本系列教程的原意——Rx入门

权威的示例

我觉得每个人都喜欢的Rx示例是——它对用户输入的搜索短语的处理,因为实在是太棒了。

事实上,我在4月的时候和两位坛友讨论过,Brent对比了传统方式和RxSwift方式的区别,基本规则是:

  • 0.3秒内的搜索文本多次改变必须合并成一次
  • 当搜索文本改变了,文本字符大于等于4个的时候,发起新的http请求,如果之前有请求未完成的话,必须先取消掉
    http请求返回数据后,刷新tableView
    还要提供一个刷新button,让用户可以马上手动发起http请求

虽然我们不得不写一些配套的基础代码,但是满足上述需求真的挺简单。核心代码在这里

let o: Observable<String> = textField.rx.text
.throttle(0.3)
.distinctUntilChanged()
.filter { query in return query.characters.count > 3 }

解释下代码作用:

  • 我们根据UITextFieldtext属性变化触发处理
  • 节流(throttling)的方法来忽略它在0.3秒内的所有变化
  • 忽略重复(和上次的值相同)的情况
  • 忽略小于4个(必须>3)字符的情况

Boom💥沙卡拉卡……这就是RxSwift炫酷的原因。

下一回

仍然还有些事没做完,因为,我们成为了糟糕的开发者,我们的代码从头到尾都没有单元测试覆盖。在本系列的第五篇,我会说说如何用RxSwift来做单元测试。就像RxSwift一样,单元测试也和我们传统方式大相径庭,但是也非常强大。

译者

我也是个bad developer,单元测试根本不懂……强行翻译怕是偏离了作者原意,误人子弟。所以,最后的第五篇我就不翻译了,大家自己看原文吧。

谢谢观看,有翻译得不好的,水平实在有限,但是又舍不得这么好的RxSwift入门明珠蒙尘,请见谅哈😆