初探 ReactiveSwift

这篇文章其实算是一篇学习笔记,先是回顾了 MVVMReactiveCocoa 的历史,然后对 ReactiveCocoa 5.0 拆分出来的 ReactiveSwift 中的一些概念结合源码做了一定的解释。

在下一篇文章中,我还会通过一个复杂度适中的案例完成一个使用 MVVM + ReactiveCocoa 的 Demo, 总结了一些开发过程中的注意要点。
(这篇太长了 +。=)

忆往昔

关于 MVCMVVM,其实已经被讨论了好几年,不知道多少年前是谁带的头,吐槽苹果官方所推荐的 MVC 模式就是 Massive View Controller,然后各路人马就费劲脑汁怎么给 ViewController 瘦身,抽出个 DataSource,抽出个ViewModel,抽出个啥啥啥,其实个人以为大部分情况下只是让 ViewController 看起来不那么臃肿,如果操作不当,可能不但没有降低其耦合性,反而可能增加了许多不必要的依赖。

MVVM 这个模式其实最早是微软提出的,结合了 MVP 的架构和WPF(微软的双向绑定)技术。

对,没错,就是双向绑定,MVVM 严重依赖于双向绑定,严格意义上来说,没有双向绑定的都不能称作是 MVVM

所以 MVVM 真正流行起来还是因为 ReactiveCocoa 的出现,借助于 ReactiveCocoa 可以实现 UIKit 组件和数据的双向绑定,也就意味着 MVVM 可以在 iOS 开发中得到更好的实践。

这篇文章ReactiveCocoa and MVVM, an Introduction (中文版的见ReactiveCocoa 和 MVVM 入门)已经把 MVCMVVM 的关系、ReactiveCocoa 中的一些核心理念(比如信号 Signal,订阅者 Subscriber)、MVVM 的优势、以及如何在 Objective-C 中实践 ReactiveCocoa 讲的相当清楚了,建议不了解的可以先读一遍。

其实这篇文章大概在2年以前我就读过,当时是同事推荐的,当时简单读了一遍,感觉很高大上,但是并不喜欢(为什么不喜欢我一会儿讲),一直以来也没自己去尝试一遍。

回过头又找到了这篇文章,翻到最下面第一条评论就是当初前同事的:

不禁想起了前司的日子,不过结局我想大家也都猜到,根本就没有推起来(我甚至不记得有推过),这也是我为什么当时并不喜欢的原因。

总结下来,原因有以下几点:

  • 代码晦涩难懂(代码里各种 RAC、RACObserve的宏,各种闭包,各种Signal,很挑战传统程序员的理解水平)
  • 学习成本高 (用的好需要懂函数式、响应式,需要理清各个概念,光函数式编程就很难说真的入门)
  • 调试成本高(RAC 严重侵入了 Cocoa 框架)

    balabala……

总之就是想在一个成型的团队推这样的编程理念,并且能够真正使用起来的确是有一定难度的。

当然,即使这样,也不能否认它的确具有一些 MVC 没有办法做到的优势,清晰统一的数据流、可测试的 ViewModel,职责更加明确的 ViewViewController


于是。。。话说回来,为什么又回头看这玩意儿呢?

其一,几年过去了,它依然很火,或者越来越火,说明的确还是很好用的,当年自己不想用或许是因为太弱了吧(虽然现在依然很弱)。

其二,找实习了,前几天面的公司说他们正在用 MVVM + FRP + ReactiveSwift,既然这样,我就再看看呗,然后发现很难找到关于 ReactiveSwift 以及 ReactiveCocoa 5.0 的相关文章,那就顺带边学边写一篇小小的总结。

ReactiveCocoa 前世今生

如果我记得没错的话, ReactiveCocoa 的框架最初是用 Objective-C 写的,上面提到的文章便是基于 Objective-C 的,充斥着大量的 RACRACObserver 的宏,以及rac_的前缀,更新到 3.0 的时候出现了 Swift 的版本,如今已经更新到 5.0 的, 而 5.0 可以说相比于之前的版本有了巨大的改动,主要体现在一下几点:

  • 适配 Swift 3.0
  • 将原有的 RAC 拆分成4个库,分别为 ReactiveCocoaReactiveSwiftReactiveObjc 以及 ReactiveObjcBridge
  • 修改了大部分核心 API 明明,去除了 rac 前缀, 统一使用 reactive 的命名空间。(个人觉得更 Swift 了)

ReactiveCocoa

如今 ReactiveCocoa 这个库主要用于为 UIKitAppKit 提供扩展,使其支持响应式编程,依赖于 ReactiveSwift,所做的更多的是业务层面的工作,主要任务就是将 UI 元素和操作转换成 ReactiveSwift 中的 SignalAction 以及 BindingTarget,其中的相关概念都将在下一节解释。

ReactiveSwift

ReactiveSwiftReactiveCocoa 拆分出来的只和 Swift 相关的核心库, 这个库非常精简,仅有十来个文件,且不依赖于平台,所以仅有 ReactiveSwift 是没法完整地完成一个 MVVM 函数响应式应用的。

ReactiveCocoa + ReactiveSwift 可以用来完成 CocoaTouch 以及 Coaco 的开发, 而毕竟 Swift 已经是一门开源的语言,仅用 ReactiveSwift 即可完成在其他应用场景下的函数响应式开发,这也是将它分离开来的原因。

ReactiveObjc

ReactiveCocoa 原有的 Objective-C 代码都被放在了这个库中,它和 ReactiveSwift 具备相同的功能,但是却有两套 API, 原先这两个都在一个库中,拆分开来是为了更好地维护。

ReactiveObjcBridge

Bridge 顾名思义,是用于处理一些历史遗留问题的,比如项目中存在 Swift 调用 Objective-C 写的 API, 那么就需要使用这个库来完成转换。

使用场景

  • 纯 Swift 开发: ReactiveCocoa + ReactiveSwift( RAC 自动依赖 )
  • 纯 Objective-C 开发: ReactiveObjc
  • Swift + Objective-C 混编: 四个库都需要

ReactiveSwift 相关概念梳理

ReactiveSwift 是一个典型的函数响应式框架,基于 数据流 的概念,提供了可组合的、可声明的、灵活的数据类型。下面先介绍一下 ReactiveSwift 中一些基本概念。

Signal 信号

1
public final class Signal<Value, Error: Swift.Error>

个人以为,信号可以理解为管道,是事件传递的通道,事件通过 Signal 来传递到订阅者那里,订阅者根据事件类型以及数据进行对应操作,需要注意的是,这个事件流是单向的,这就意味着订阅者不会对信号本身带来任何影响 —— 即无副作用。

定义一个信号时,需要制定信号所传输的数据的类型( Value ) 以及错误类型 ( Error ),错误类型需要遵循 Swift 中的 Error 协议,如果确定不会有错误发生,可以直接用 Result 内置的 NoError

信号创建的几个方法

  • pipe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let (signal, observer) = Signal<Int, NoError>.pipe()
let subscriber1 = Observer<Int, NoError>(value: { print("Subscriber 1 received \($0)") } )
let subscriber2 = Observer<Int, NoError>(value: { print("Subscriber 2 received \($0)") } )
print("Subscriber 1 subscribes to the signal")
signal.observe(subscriber1)
print("Send value `10` on the signal")
observer.send(value: 10)
// 订阅者此时会收到消息
print("Subscriber 2 subscribes to the signal")
// 因为subscribe2此时还没有注册订阅,所以并不会收到之前的消息
signal.observe(subscriber2)
print("Send value `20` on the signal")
// Notice that now, subscriber1 and subscriber2 will receive the value
observer.send(value: 20)
// 此时两个订阅者都可以收到消息

pipe 方法是 Signal 类型的一个静态方法,会调用 Signal 的指定初始化方法,生成 Observer 并返回一个 SignalObserver 的元组,返回的 Observer 相当于事件的发送者。

订阅者同样也是 Observer 类型,即观察者,可以调用 signal.observe() 方法注册订阅者,如上述代码所示,一旦信号发送事件,订阅者便会得知,根据事件类型执行闭包中的操作。

  • 事件绑定
1
2
3
4
5
6
7
8
9
extension Reactive where Base: UITextField {
/// A signal of text values emitted by the text field upon any changes.
///
/// - note: To observe text values only when editing ends, see `textValues`.
public var continuousTextValues: Signal<String?, NoError> {
return controlEvents(.editingChanged).map { $0.text }
}
}
  • 通过 property 获取 (见下文)
  • 通过 signalProducer 创建(见下文)

这是 ReactiveCocoa 中的代码,可以通过调用 textField.reactive.continuousTextValues 来获取一个 Signal,这个信号用于传递输入框中内容变化的,每次输入框中内容出现变话,都会发送一个类型为 Value 的事件附带输入框中内容的 value,实际上 Observer 观察的是 .editingChanged 事件。

信号的转换

map 函数为例

1
2
3
4
5
6
7
let (signal, observer) = Signal<Int, NoError>.pipe()
let subscriber = Observer<Int, NoError>(value: { print("Subscriber received \($0)") } )
let mappedSignal = signal.map { $0 * 2 }
mappedSignal.observe(subscriber)
print("Send value `10` on the signal")
observer.send(value: 10)
//输出Subscriber received 20

ReactiveSwift 内置的相关还是还有很多,比如flatMapfilter等等,用于对信号传递的值进行再处理,顺带说一句,ReactiveSwift 中几乎所有的元素都是可以执行这些函数式的操作进行转换的。

Event 事件

EventSignal 传递的单元, 在 ReactiveSwift 中,Event 是一个枚举类型,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum Event<Value, Error: Swift.Error> {
/// 提供值类型.
case value(Value)
/// The signal terminated because of an error. No further events will be
/// received.
/// 失败,一旦收到失败类型,信号之后就不会发送任何消息
case failed(Error)
/// The signal successfully terminated. No further events will be received.
/// 正常结束
case completed
/// Event production on the signal has been interrupted. No further events
/// will be received.
///
/// - important: This event does not signify the successful or failed
/// completion of the signal.
///中断
case interrupted
}

Observer 观察者

观察者,实际上 Event 仅仅封装了一个 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public final class Observer<Value, Error: Swift.Error> {
//Action实际上是一个闭包
public typealias Action = (Event<Value, Error>) -> Void
public let action: Action
public init(_ action: @escaping Action) {
self.action = action
}
//初始化方法传入4个类型的闭包,分别处理四种不同的事件(当然,是可选的)
public convenience init(
value: ((Value) -> Void)? = nil,
failed: ((Error) -> Void)? = nil,
completed: (() -> Void)? = nil,
interrupted: (() -> Void)? = nil
) {
self.init { event in
switch event {
case let .value(v):
value?(v)
case let .failed(error):
failed?(error)
case .completed:
completed?()
case .interrupted:
interrupted?()
}
}
}
}

ObserverProtocol 定义了观察者发送事件的接口,实现起来也非常简单,就是分别发送四个不同类型的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
extension Observer: ObserverProtocol {
/// Puts a `value` event into `self`.
///
/// - parameters:
/// - value: A value sent with the `value` event.
public func send(value: Value) {
action(.value(value))
}
/// Puts a failed event into `self`.
///
/// - parameters:
/// - error: An error object sent with failed event.
public func send(error: Error) {
action(.failed(error))
}
/// Puts a `completed` event into `self`.
public func sendCompleted() {
action(.completed)
}
/// Puts an `interrupted` event into `self`.
public func sendInterrupted() {
action(.interrupted)
}
}

Property 可被观察的值的封装

1
2
3
4
5
6
7
8
public final class Property<Value>: PropertyProtocol {
private let disposable: Disposable?
private let _value: () -> Value
private let _producer: () -> SignalProducer<Value, NoError>
private let _signal: () -> Signal<Value, NoError>
.......
}

Property 封装了一个 Value ,同时持有一个 signalsignalProducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
let mutableProperty = MutableProperty(1)
// property 的值可以直接通过 property.value 获取
print("Property has initial value \(mutableProperty.value)")
// 可以通过 property.producer 或者 property.signal 来订阅 Property 的变化,差别在于 producer 会通知初始值, 而 signal 仅有在更新的时候会通知
mutableProperty.producer.startWithValues {
print("mutableProperty.producer receied \($0)")
}
mutableProperty.signal.observeValues {
print("mutableProperty.signal received \($0)")
}
print("---")
print("Setting new value for mutableProperty: 2")
mutableProperty.value = 2
print("---")
// Property 不可变的, MutableProperty 是可变的
let property = Property(mutableProperty)
print("Reading value of readonly property: \(property.value)")
property.signal.observeValues {
print("property.signal received \($0)")
}
// Its not possible to set the value of a Property
// readonlyProperty.value = 3
// But you can still change the value of the mutableProperty and observe its change on the property
print("---")
print("Setting new value for mutableProperty: 3")
mutableProperty.value = 3
// Constant properties can be created by using the `Property(value:)` initializer
let constant = Property(value: 1)
// constant.value = 2 // The value of a constant property can not be changed
////输出
Property has initial value 1
mutableProperty.producer receied 1
---
Setting new value for mutableProperty: 2
mutableProperty.producer receied 2
mutableProperty.signal received 2
---
Reading value of readonly property: 2
---
Setting new value for mutableProperty: 3
mutableProperty.producer receied 3
mutableProperty.signal received 3
property.signal received 3

SignalProducer 信号生产者

关于 SignalSignalProducer 的关系,以及 SignalProducer 的用途其实比较难理解,这里结合具体实例来解释可能更加直白一点。

在官方文档中的描述是这样的:

SignalProducer 用于创建信号,并且可以执行副作用

还有一个关键词就是——deferred work,延迟生效,并且每次被 start 都会重新创建一个新的 Signal

先看一下 SignalProducer 的几种用法:

  • 闭包创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let producer = SignalProducer<Int, NoError> { observer, _ in
print("New subscription, starting operation")
observer.send(value: 1)
observer.send(value: 2)
}
let subscriber1 = Observer<Int, NoError>(value: { print("Subscriber 1 received \($0)") })
let subscriber2 = Observer<Int, NoError>(value: { print("Subscriber 2 received \($0)") })
print("Subscriber 1 subscribes to producer")
producer.start(subscriber1)
print("Subscriber 2 subscribes to producer")
// Notice, how the producer will start the work again
producer.start(subscriber2)
//输出
Subscriber 1 subscribes to producer
New subscription, starting operation
Subscriber 1 received 1
Subscriber 1 received 2
Subscriber 2 subscribes to producer
New subscription, starting operation
Subscriber 2 received 1
Subscriber 2 received 2
  • 通过事件创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var value: Int?
SignalProducer<Int, NoError>(value: 42)
.on(value: {
value = $0
})
.startWithSignal { signal, disposable in
print(value)
}
print(value)
//输出
nil
Optional(42)
  • 通过 signal 创建(一般用于处理事件响应)
1
2
let (signal, observer) = Signal<Bool,NoError>.pipe()
let signalProducer = SignalProducer<Bool, NoError>(signal)

关于 副作用 ,英文的说法是 side effect,这里也实在找不到什么更好的翻译了,而且的确挺难理解。

我们举一个例子,假设我们要实现一个按钮点击实现,点击按钮执行一个网络请求,请求完毕后需要处理请求的结果,一般会通过 signalProducer 来实现, 首先,网络请求是异步的,你需要提前预置好异步请求结果的处理方法,再者,signalProducer有一个on方法,可以处理在整个过程中针对任何事件的回调,这是单纯的 Signal 无法做到的。

1
2
3
4
5
6
7
8
9
let (signal, observer) = Signal<Bool,NoError>.pipe()
let signalProducer = SignalProducer<Bool, NoError>(signal).on(
starting: nil, started: nil,
event: {
_ in
//做一些操作
},
value: nil, failed: nil, completed: nil, interrupted: nil, terminated: nil, disposed: nil
)

Action 动作

先看一下 Action 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class Action<Input, Output, Error: Swift.Error> {
private let executeClosure: (_ state: Any, _ input: Input) -> SignalProducer<Output, Error>
private let eventsObserver: Signal<Event<Output, Error>, NoError>.Observer
private let disabledErrorsObserver: Signal<(), NoError>.Observer
/// The lifetime of the Action.
public let lifetime: Lifetime
/// A signal of all events generated from applications of the Action.
///
/// In other words, this will send every `Event` from every signal generated
/// by each SignalProducer returned from apply() except `ActionError.disabled`.
public let events: Signal<Event<Output, Error>, NoError>
.........
}

Action 有一个 Input 代表输入, Output 代表输出, Action<Output, Error> 对应于其 Signal<Value, Error>

executeClosure 是一个生成 SignalProducer 的闭包,在 Action 的初始化方法中,会生成对应的 eventObserverevents 的 Signal, 用于观察和发送事件。

Action 的一个核心方法是 applyapply 会执行 executeClosure ,生成一个 signalProducer 并返回。

这里我们用官方文档里的一个比较形象的案例来解释一下其工作流程。

我们可以将 Action 类比成一个自动取款机,一旦 Action 被创建, 我们只需要选择选项投入硬币,机器便会自动地执行预置的操作,最终输出你想要的商品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// The vending machine.
class VendingMachine {
//purchase 是一个 Action, 输入是 Int 即零食id, 输出是 Snack 即零食
let purchase: Action<Int, Snack, VendingMachineError>
//取款机会记录当前已经投入的硬币,这是一个被保存的状态
let coins: MutableProperty<Int>
// The vending machine is connected with a sales recorder.
init(_ salesRecorder: SalesRecorder) {
coins = MutableProperty(0)
//创建 purchase 这个 action
purchase = Action(state: coins, enabledIf: { $0 > 0 }) { coins, snackId in
return SignalProducer { observer, _ in
// The sales magic happens here.
// Fetch a snack based on its id
// 可以在 SignalProducer 的执行闭包中处理输出商品的逻辑
// 在最后一遍会执行
// observer.send(....)
// observer.sendCompleted()
}
}
// The sales recorders are notified for any successful sales.
//可以通过 action.values.observeValues 订阅 Signal
purchase.values.observeValues(salesRecorder.record)
}
}
// 执行apply,这里输入 snackId,生成 SignalProducer 并 start
// Purchase from the vending machine with a specific option.
vendingMachine.purchase
.apply(snackId)
.startWithResult { result
switch result {
case let .success(snack):
print("Snack: \(snack)")
case let .failure(error):
// Out of stock? Insufficient fund?
print("Transaction aborted: \(error)")
}
}

Lifetime 生命周期

LifetimeReactiveCocoa 5.0 后出现的一个概念,通过提供一个 Lifetime,可以控制观察的生命周期

1
2
3
4
5
6
7
8
class VideoPlayer {
private let (lifetime, token) = Lifetime.make()
func play() {
let frames: SignalProducer<VideoFrame, ConnectionError> = ...
frames.take(during: lifetime).start { frame in ... }
}
}

Lifetime 的实现比较简单, Observation 的生命周期即 token 的生命周期,一旦 token 被销毁, token 所持有的 endedObserver 便会发送一个 completed 的事件, Lifetime 订阅了 ended 的 signal,会收到一个 completed 的信号, 从而再通知注册了 lifetimeSignal 停止数据流。

1
2
3
4
5
6
7
8
9
10
11
public final class Token {
/// A signal that sends a Completed event when the lifetime ends.
fileprivate let ended: Signal<(), NoError>
private let endedObserver: Signal<(), NoError>.Observer
public init() {
(ended, endedObserver) = Signal.pipe()
}
deinit {
endedObserver.sendCompleted()
}
}

<~ 绑定

ReactiveSwift 中, 我们常常看到 <~ 操作符,这是一个非常直观的符号,其含义是绑定,值得注意的是,这个绑定是单向。

1
BindingTargetProtocol <~ BindingSourceProtocol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public func <~
<Target: BindingTargetProtocol, Source: BindingSourceProtocol>
(target: Target, source: Source) -> Disposable?
where Source.Value == Target.Value, Source.Error == NoError
{
// Alter the semantics of `BindingTarget` to not require it to be retained.
// This is done here--and not in a separate function--so that all variants
// of `<~` can get this behavior.
let observer: Observer<Target.Value, NoError>
if let target = target as? BindingTarget<Target.Value> {
observer = Observer(value: { [setter = target.setter] in setter($0) })
} else {
observer = Observer(value: { [weak target] in target?.consume($0) })
}
return source.observe(observer, during: target.lifetime)
}

SignalSignalProducerProperty 都是 BindingSourceProtocal

PropertyAction 可以是 BindingTargetProtocal

  • Property <~ SignalProducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let producer = SignalProducer<Int, NoError> { observer, _ in
print("New subscription, starting operation")
observer.send(value: 1)
observer.send(value: 2)
}
let property = MutableProperty(0)
property.producer.startWithValues {
print("Property received \($0)")
}
property <~ producer
//输出
Property received 0
New subscription, starting operation
Property received 1
Property received 2
  • Property <~ Signal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let (signal, observer) = Signal<Int, NoError>.pipe()
let property = MutableProperty(0)
property.producer.startWithValues {
print("Property received \($0)")
}
property <~ signal
print("Sending new value on signal: 1")
observer.send(value: 1)
print("Sending new value on signal: 2")
observer.send(value: 2)
//输出
Property received 0
Sending new value on signal: 1
Property received 1
Sending new value on signal: 2
Property received 2
  • Property <~ Property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let property = MutableProperty(0)
property.producer.startWithValues {
print("Property received \($0)")
}
let otherProperty = MutableProperty(0)
// Notice how property receives another value of 0 as soon as the binding is established
property <~ otherProperty
print("Setting new value for otherProperty: 1")
otherProperty.value = 1
print("Setting new value for otherProperty: 2")
otherProperty.value = 2
//输出
Property received 0
Property received 0
Setting new value for otherProperty: 1
Property received 1
Setting new value for otherProperty: 2
Property received 2

当然,我相信其实大家见的最多的 <~ 符号应该出现在这种情况下

1
verifyButton.reactive.isEnabled <~ viewModel.canSendAuthCode

这里 canSendAuthCode 是一个 Bool 类型的 Property,而 Button.reactive.isEnabled 实际上是在 ReactiveCocoa 中给 UIControlisEnabled 定义的 BindingTargetBindingTarget 遵循 BindingTargetProtocal 协议

总结

如上,结合源码对 ReactiveSwift 中的一些核心概念做了介绍。

但是,请相信我,就算我写得再详细(何况写的还是不够好),不自己动手实践一下还是无法真正理解这些元素的用途。

那么,我建议你,在理解了基本概念后,自己动手写一个小 Demo

当然,我也写了一个基于 MVVM + ReactiveSwift 的小 Demo ,将在下一篇文章中放出,并且描述一下新路历程,结合现有资料总结下 MVVM + RAC 下好的实践的注意点。

参考资料