iOS 开发中的 ReactiveCocoa (RAC)

由浅入深,从基本概念到源码解析,带你全面掌握 Swift 响应式编程


一、什么是响应式编程?

1.1 从命令式到声明式

传统的命令式编程中,我们通过直接修改状态和调用方法来驱动程序执行:

1
2
3
4
5
6
7
8
9
// 命令式:监听文本框变化
textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)

func textDidChange() {
let text = textField.text ?? ""
if text.count >= 3 {
searchUsers(text)
}
}

响应式编程则将数据流视为核心概念,用声明式的方式描述「当数据变化时该做什么」:

1
2
3
4
5
6
7
// 响应式:声明数据流关系
textField.reactive.continuousTextValues
.filter { ($0?.count ?? 0) >= 3 }
.debounce(0.3, on: QueueScheduler.main)
.observeValues { [weak self] text in
self?.searchUsers(text ?? "")
}

1.2 ReactiveCocoa 与 ReactiveSwift

  • ReactiveSwift:纯 Swift 实现的响应式核心库,提供 Signal、SignalProducer 等基础类型
  • ReactiveCocoa:基于 ReactiveSwift,为 Cocoa/Cocoa Touch 提供 UI 绑定、扩展和便捷 API

两者关系可以理解为:ReactiveSwift 是引擎,ReactiveCocoa 是上层封装。

1.3 为什么选择 RAC?

  • 统一抽象:将 delegate、回调、通知、KVO、Target-Action 等统一为「事件流」
  • 可组合:通过 map、filter、combineLatest 等操作符组合数据流
  • 减少状态:用数据流替代分散的中间变量
  • 声明式:代码更贴近「业务意图」,易于阅读和维护

二、核心概念

2.1 事件 (Event)

Event 是事件流中的最小传输单元,类似一次性的直播流中的一帧:

1
2
3
4
5
6
public enum Event<Value, Error: Swift.Error> {
case value(Value) // 携带一个值
case failed(Error) // 失败(携带错误)
case completed // 正常完成
case interrupted // 被中断
}

一个典型的流:若干个 .value,最后以 .completed.failed 结束;.interrupted 表示订阅被取消。

2.2 观察者 (Observer)

Observer 负责向事件流发送事件:

1
2
3
4
5
6
let (signal, observer) = Signal<String, Never>.pipe()

// observer 用于发送事件
observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()

2.3 可销毁对象 (Disposable)

订阅信号会返回 Disposable,用于取消订阅、释放资源:

1
2
3
4
5
6
let disposable = signalProducer.start { value in
print(value)
}

// 不再需要时取消订阅
disposable.dispose()

2.4 核心类型总览

类型 描述 类比
Signal 热信号,单向事件流,所有者控制发送 直播画面
SignalProducer 冷信号,延迟执行,每次 start 创建新流 点播视频
Property 可观察的「有值」盒子,永不失败 播放进度条
Action 串行执行、可启用/禁用的操作 自动售货机
Lifetime 观察的生命周期,用于自动取消 观看时间段

三、Signal 与 SignalProducer

3.1 Signal:热信号

  • 热信号:无论有没有观察者,事件都会持续发送
  • 所有者完全控制何时发送、发送什么
  • 观察者只能订阅,不能影响流的产生
1
2
3
4
5
6
7
8
9
10
// 创建 Signal 的方式:pipe
let (signal, observer) = Signal<String, Never>.pipe()

signal.observeValues { value in
print("收到: \(value)")
}

observer.send(value: "A") // 输出: 收到: A
observer.send(value: "B") // 输出: 收到: B
observer.sendCompleted()

3.2 SignalProducer:冷信号

  • 冷信号:只有在 start 时才真正执行
  • 每次 start 都会创建新的 Signal,重新跑一遍逻辑
  • 适合网络请求、文件读取等「按需执行」的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let producer = SignalProducer<String, Never> { observer, _ in
print("开始执行") // 每次 start 都会打印
observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()
}

let d1 = producer.startWithValues { print("观察者1: \($0)") }
// 输出: 开始执行
// 输出: 观察者1: Hello
// 输出: 观察者1: World

let d2 = producer.startWithValues { print("观察者2: \($0)") }
// 再次输出: 开始执行
// 输出: 观察者2: Hello
// 输出: 观察者2: World

3.3 热信号 vs 冷信号

特性 Signal (热) SignalProducer (冷)
执行时机 由发送者决定 start 时执行
多订阅 共享同一流 每个订阅独立执行
典型场景 按钮点击、通知 网络请求、文件读取

四、Property 与 Action

4.1 Property:可观察的盒子

Property 表示「始终有一个当前值」且「永不失败」的流,适合表示 UI 状态、配置等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MutableProperty:可读可写
let username = MutableProperty<String>("")

// 读取当前值
print(username.value)

// 监听变化
username.signal.observeValues { newValue in
print("用户名变为: \(newValue)")
}

// 更新值
username.value = "张三"
username.modify { $0 = "李四" }

4.2 Action:串行操作

Action 将「输入 → 输出」封装为可复用、可启用/禁用的操作,且保证串行执行(同一时间只处理一个请求):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let searchAction = Action<String, [User], NSError> { keyword in
return searchAPI(keyword) // 返回 SignalProducer
}

// 绑定启用条件(例如:输入非空)
let enabledSearch = Action(state: viewModel.keyword, enabledIf: { $0.count > 0 }) { _, keyword in
return searchAPI(keyword)
}

// 执行
searchAction.apply("Swift").startWithResult { result in
switch result {
case .success(let users):
self.users = users
case .failure(let error):
self.showError(error)
}
}

五、常用操作符

5.1 转换类

map:转换每个值

1
2
3
signal
.map { $0.uppercased() }
.observeValues { print($0) }

filter:过滤值

1
2
3
signal
.filter { $0.count >= 3 }
.observeValues { print($0) }

reduce:聚合为单个值(在 completed 时发出)

1
2
3
signal
.reduce(0) { $0 + $1 }
.observeValues { print("总和: \($0)") }

5.2 组合类

combineLatest:合并多个流的最新值

1
2
3
4
let combined = Signal.combineLatest(usernameSignal, passwordSignal)
combined.observeValues { (user, pwd) in
loginButton.isEnabled = user.count > 0 && pwd.count >= 6
}

zip:按顺序一一配对

1
2
let zipped = Signal.zip(numbersSignal, lettersSignal)
// (1,A), (2,B), (3,C)...

5.3 扁平化 (flatten)

merge:内层多个流的值按到达顺序全部输出

concat:按顺序消费内层流,前一个完成后才订阅下一个

latest:只保留「最新」的内层流,常用于搜索联想(新关键词来时取消旧请求)

1
2
3
4
5
6
7
searchKeywordSignal
.flatMap(.latest) { keyword in
searchAPI(keyword) // 新关键词会取消上一次请求
}
.observeValues { users in
self.updateUI(users)
}

5.4 错误处理

1
2
3
4
producer
.flatMapError { _ in SignalProducer(value: []) } // 失败时返回空数组
.retry(upTo: 3) // 失败重试 3 次
.mapError { CustomError.wrapped($0) } // 转换错误类型

六、源码与实现原理

6.1 Signal 的核心结构

Signal 通过闭包保存「如何向观察者推送事件」的逻辑,内部维护观察者列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Signal 的核心:Generator 闭包
// 当有观察者订阅时,闭包被调用,传入 Observer
public init(_ generator: (Observer) -> Disposable?)

// pipe 创建方式
public static func pipe() -> (Signal, Signal.Observer) {
var observer: Signal.Observer!
let signal = Signal { innerObserver in
observer = innerObserver
return nil
}
return (signal, observer)
}

observer.send(value:) 被调用时,所有已注册的观察者都会收到该值。

6.2 SignalProducer 的延迟执行

SignalProducer 保存的是「如何创建 Signal」的闭包,而不是 Signal 本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 每次 start 时:
// 1. 创建一个新的 Signal(通过 pipe 或类似机制)
// 2. 执行 generator 闭包,将 observer 传入
// 3. 闭包内的逻辑开始执行,向 observer 发送事件
public func start(_ observer: Observer) -> Disposable {
let (signal, pipeObserver) = Signal.pipe()
let disposable = CompositeDisposable()
disposable += signal.observe(observer)
disposable += self.startWithSignal { signal, innerDisposable in
pipeObserver.observe(signal)
disposable += innerDisposable
}
return disposable
}

因此每次 start 都会触发一次完整的「创建 + 执行」过程。

6.3 操作符的链式调用

mapfilter 等操作符本质是创建新的 Signal/SignalProducer,内部订阅上游并转换后传给下游:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// map 的简化逻辑
extension Signal {
public func map<U>(_ transform: @escaping (Value) -> U) -> Signal<U, Error> {
return Signal { observer in
return self.observe { event in
switch event {
case let .value(value):
observer.send(value: transform(value))
case .completed:
observer.sendCompleted()
case let .failed(error):
observer.send(error: error)
case .interrupted:
observer.sendInterrupted()
}
}
}
}
}

七、实战示例

7.1 登录表单校验

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
class LoginViewModel {
let username = MutableProperty("")
let password = MutableProperty("")

var canLogin: Property<Bool> {
return Property(
initial: false,
then: Signal.combineLatest(username.signal, password.signal)
.map { user, pwd in
user.count >= 3 && pwd.count >= 6
}
)
}

let loginAction: Action<(String, String), User, Error>

init() {
loginAction = Action(enabledIf: canLogin) { [weak self] _, input in
return loginAPI(username: input.0, password: input.1)
}
}
}

// ViewController 中绑定
viewModel.canLogin.signal
.observeValues { [weak loginButton] canLogin in
loginButton?.isEnabled = canLogin
}

loginButton.reactive.pressed = CocoaAction(viewModel.loginAction) { _ in
(viewModel.username.value, viewModel.password.value)
}

7.2 搜索联想(防抖 + 取消旧请求)

1
2
3
4
5
6
7
8
9
10
11
12
13
searchTextField.reactive.continuousTextValues
.filter { ($0 ?? "").count >= 2 }
.debounce(0.3, on: QueueScheduler.main)
.flatMap(.latest) { [weak self] keyword -> SignalProducer<[User], Error> in
guard let keyword = keyword, !keyword.isEmpty else {
return .init(value: [])
}
return self?.searchAPI(keyword) ?? .empty
}
.observe(on: UIScheduler())
.startWithValues { [weak self] users in
self?.updateSearchResults(users)
}

7.3 多数据源合并展示

1
2
3
4
5
6
7
8
9
10
11
12
13
let localUsers = loadLocalUsers()   // SignalProducer<[User], Never>
let remoteUsers = fetchRemoteUsers() // SignalProducer<[User], Error>

SignalProducer.combineLatest(
localUsers.flatMapError { _ in .init(value: []) },
remoteUsers.flatMapError { _ in .init(value: []) }
)
.map { local, remote in
mergeAndDeduplicate(local: local, remote: remote)
}
.startWithValues { [weak self] users in
self?.tableView.reload(with: users)
}

八、ReactiveCocoa 的 Cocoa 扩展

ReactiveCocoa 为 UIKit 提供了 reactive 命名空间下的扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
// UIControl 事件
button.reactive.controlEvents(.touchUpInside)
.observeValues { _ in print("点击") }

// UITextField 文本
textField.reactive.continuousTextValues
.observeValues { print($0 ?? "") }

// 双向绑定(需要导入 ReactiveCocoa)
label.reactive.text <~ viewModel.title // 单向:ViewModel -> Label

// CocoaAction 绑定按钮
button.reactive.pressed = CocoaAction(viewModel.submitAction)

九、最佳实践与注意事项

  1. 避免循环引用:在闭包中使用 [weak self],在适当时机 dispose 订阅
  2. 主线程更新 UI:使用 .observe(on: UIScheduler()) 确保 UI 更新在主线程
  3. 合理选择热/冷信号:异步任务、网络请求用 SignalProducer;UI 事件、通知用 Signal
  4. 错误类型:尽量使用 Never 表示不会失败的流,便于组合
  5. 利用 Lifetime:在 ViewController/View 销毁时通过 Lifetime 自动取消订阅

十、总结

概念 要点
Event 事件流的传输单元:value / failed / completed / interrupted
Signal 热信号,由所有者控制,多订阅共享
SignalProducer 冷信号,延迟执行,每次 start 独立运行
Property 有当前值、不失败的流,适合状态
Action 串行、可启用/禁用的操作封装
操作符 map、filter、combineLatest、flatMap 等组合与转换流
实践 表单校验、搜索联想、多源合并、UI 绑定

掌握这些概念后,你就能用响应式思维重构业务逻辑,写出更清晰、更易维护的 Swift/iOS 代码。


参考资源