[翻译]RxSwift入门(2)

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

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

为了学习RxSwift,我准备了一个完全用原生UIKit编写的demo,看上去很牵强附会的app。我们可以一步步地用RxSwift把它转化。开头这几步会稍显笨拙,但是可以把新的编程概念润物细无声地灌输给我们。

在本入门系列的第一篇,我们已经看过app的样子了:(再次盗图)



这个app是用storyboard构建的(通常我更喜欢用XIB,以后再讨论),有一个UIViewController,它的代码如下:

import UIKit

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

如你所见,也没多少。用UILabel显示button被点击了多少次。button(对应的变量)并没有储存在类中,因为它是通过IBAction连线得来的,这里没必要储存它。

遗憾的是,我们必须自行跟踪button被点击了多少次,其储存在count里。

这个@IBAction func onButtonTap(sender:)方法就是刚刚提到的IBAction,从Interface Builder连线而来,当button被点击的时候由UIKit调用。

这些代码都是小菜一碟,不值一提。

转换到Rx

转换成用Rx的第一步是:思考什么是输入,什么是输出。是什么引起了变化?是什么引起了我们去计算,导致我们展示不同的东西给用户看?

在这个简单的app里,很明显是UIButton被点击导致发生计算,改变了某个状态。只要button被点击,我们就改变count的值。

弹珠图(Marble Diagrams)

这种状态的变化是怎样的呢?我们把它基于时间建模:

---[tap]---[tap]---[tap]--->

相当于:

---[ 1 ]---[ 2 ]---[ 3 ]--->

上面这种描述是一种很简陋的弹珠图。弹珠图是在Rx世界中描述信号的一种方式。横条代表时间。显而易见,我们从左边开始,朝着右边进行。在上图的每一次点击,都会在下图产生一个与之对应的不同值。

弹珠图是一种很棒的方式去展示操作符在Rx中是如何工作的。其中一个例子是map:上面是输入,下面是输出,map操作在中间:



从上图来看,map简单地把输入值乘以10,所以1变10,2变20,3变30。

旁注:你可能会注意到有些弹珠图的箭头并不仅仅有箭头,有时候还会以|X结束。一个|(管道符号)表示流处于completed,这些流就不会再发出信号。一个X表示一个error。流一旦发生了error,就再也不会发出信号事件了。

看回我们的流:

---[tap]---[tap]---[tap]--->

相当于:

---[ 1 ]---[ 2 ]---[ 3 ]--->

显然,我们从UIButton被点击这里入手。为了访问button,我们需要把它添加到view controller里,如下:

@IBOutlet weak var button: UIButton!

RxCocoa

RxSwift是一个基础套件,它可以作用于任何Swift的东西,而并不专门针对用户界面、网络请求等。而RxCocoa是以Rx的方式包装了UIKit的套件。想方便地和用户界面打交道的话,你需要:

import RxSwift
import RxCocoa

大多数UIKit的组件都有对应的响应式扩展,通常以rx属性暴露给开发者。

因此,想在view controller里获得表示button点击的流,我们需要用button.rx.tap

Observables(可被观察的事物)

button.rx.tap是一个计算属性,它返回ControlEvent类型的对象。ControlEvent是一种特殊的Observable

(敲黑板,重点)每次我在说“”的时候,其实我指的是“Observable

Rx中的表现形式就是Observables。你可以对Observables进行各种操作;RxMarbles网站就是为此而生的。

你在Rx中用到的大多数东西都是相关联的,可以被转化成Observable。事实上,大多数高阶类型如ControlEvent都可以用.asObservable()来转化为Observables

今天主要的任务就是记住:Observable是简单地表达基于时间变化的事件流。

Subscriptions(订阅)

通俗点来说,你对Observable进行的最后一个操作——在流发出信号的时候作出行动,就叫做Subscriptions(订阅)。在我们的app里,我们要在button每次被点击的时候做些什么呢?

我们会subscribe这个Observable。subscribe的时候允许我们传一个闭包执行我们的代码。现在我们的代码变成这样:

self.button.rx.tap
.subscribe(onNext: { _ in
})

那当Observable发射信号的时候,我们在闭包里要干啥呢?

穿针引线

这步,我们简单地用回我们之前写的过程式版本的一个方法:@IBAction func onButtonTap(sender:)。在Rx的世界里,这并不是正确的方式,我们这里徐徐图之,一次一小步。现在,代码变成这样了:

self.button.rx.tap
.subscribe(onNext: { _ in
self.onButtonTap(sender: self.button)
})

现在我们用不上@IBAction了(拿到了button变量,不需要连线了),可以去掉sender参数,让代码看起来更简洁:

self.button.rx.tap
.subscribe(onNext: { _ in
self.onButtonTap()
})

Disposables(可被丢弃的)

原则上,我们已经可以编译运行了,一切正常。然而,如果真的这样做的话你会发现收到编译器警告:

RxSwift里,清理和终止Observables是十分重要的事,特别是在网络请求的时候。在深入了解之前我们可以简单粗暴地记住:当你收到上述警告,就把那个对象添加到一个DisposeBag(清理袋)里。

这里,我们添加一个DisposeBagViewController里,因为这个订阅(subscription)的生命周期和我们的view controller是相关联的。

private let disposeBag = DisposeBag()

然后我们在subscribe()之后使用它:

self.button.rx.tap
.subscribe(onNext: { _ in
self.onButtonTap()
})
.addDisposableTo(self.disposeBag)

一般来说,每个class/struct在进行subscribe()的时候都可以共用一个DisposeBag,所有订阅(subscriptions)都添加到里面,就这么简单。

Debugging

就我们上面的代码而言,可以运行 ,也工作正常。然而,如果我们想在Observable链中调试(debug)呢?当然,我们可以在subscribe()的闭包里面打断点,但是有时候你想观察这个流程,却没有闭包去打断点。

还好,RxSwift提供了一个简便的方法:debug()。我们来改造一下我们的链以包含debug()

self.button.rx.tap
.debug("button tap")
.subscribe(onNext: { [unowned self] _ in
self.onButtonTap()
}).addDisposableTo(disposeBag)

现在把app跑起来,然后点击button三次,控制台输出:

2016-12-15 19:02:31.396: button tap -> subscribed
2016-12-15 19:02:34.045: button tap -> Event next(())
2016-12-15 19:02:34.584: button tap -> Event next(())
2016-12-15 19:02:35.161: button tap -> Event next(())

调用debug()会告诉我们Observable何时被订阅了,同时每次有事件发生时也会告诉我们。就像前面讨论过的,Observables可以发出这些信号:

  • Next(带一个值)
  • Error(带一个error;弹珠图用X表示)
  • Completed(弹珠图用|表示)

这些信号都会被debug()显示出来。

尽管有点难说清楚,但是debug()确实把信号带有的值也显示出来了。就我们的例子来说,button点击这个行为并不单纯是一个ControlEvent,实际上是一个ControlEvent<Void>。这是由于button的点击没有其他数据产生,仅仅发生了点击。相对而言,对于一个UISegmentedControl,它的流是一个ControlEvent<Int>。这个Int是segment的已选值。如果segment发出的信号没有带上新的选项值,那这个信号有什么意义呢?

回到我们的button点击上,ControlEvent<Void>是一种特殊的Observable,它不带有值,或者说它带的值是Void。在Swift里,Void通常可以用()表示,这就是为什么我们在控制台看到的输出是Event next(());这个也可以写成Event next(Void)

相比之下,如果我们发射的信号带有当前的点击计数,也许经过一个map之后(下篇再讨论),会变成下面这样:

2016-12-15 19:02:31.396: button tap -> subscribed
2016-12-15 19:02:34.045: button tap -> Event next(1)
2016-12-15 19:02:34.584: button tap -> Event next(2)
2016-12-15 19:02:35.161: button tap -> Event next(3)

开头你会觉得debug()把控制台搞得眼花缭乱,然而,当我们学习了后面的几篇文章之后,你会发现它很强大,以至于可以让你洞察数据是如何在流中传递的。

下一步

现在,我们已经把界面用Rx搭建好了,但是目前我们并没有任何得益。我们的代码只是换汤不换药——用不同的方式去调用旧代码罢了,别着急,我们已经一步一步接近正确的Rx实现方式了。

下一篇文章,我们开始为view controller探索更Rx方式的实现,包括最重要的一步:摆脱count变量。