学习 iOS 开发时,迟早会遇到 RxSwift 或 Combine 这道墙。招聘信息里几乎必然出现它们,但打开代码后,看到 map、flatMap、sink连成一串的陌生语法,很容易感到困惑。“明明用闭包和代理也能正常运行,真的有必要学这个吗?”产生这种疑问很自然。
今天先不讨论语法,而是从 为什么需要这类工具开始梳理。理解原因后,语法自然就能跟上。
iOS 应用其实是“事件处理机器”
拆解应用的工作内容后会发现,大多数事情都是响应事件。
- 用户点击按钮 → 切换界面
- 网络响应到达 → 刷新列表
- 键盘弹出 → 将输入框向上推
- 文本框值发生变化 → 重新请求搜索结果
问题在于,UIKit 会以 各不相同的方式传递这些事件。
| 事件源 | 传递方式 |
|---|---|
| 按钮点击 | target-action |
| 表格视图滚动 | delegate |
| 网络响应 | completion handler(闭包) |
| 键盘出现 | NotificationCenter |
| 对象属性变化 | KVO(Key-Value Observing) |
在同一个界面同时使用这五种方式,在 iOS 中非常常见。同样是“响应事件”,代码却分散成五种形式,因此要理解一个界面的行为,就必须翻查视图控制器的各个位置。
RxSwift 和 Combine 要解决的第一个问题就是这个。它们将 所有事件统一为“随时间流动的值流”。按钮点击、网络响应和键盘通知都可以通过同一个接口处理。
依靠回调硬撑时会遇到的两道墙
墙 1:组合异步任务
常见需求是:“同时请求个人信息和最近文章,等两者都到达后再绘制界面”。如果用 completion handler 编写,会变成这样。
var user: User?
var posts: [Post]?
func loadProfile() {
let group = DispatchGroup()
group.enter()
api.fetchUser { result in
user = try? result.get()
group.leave()
}
group.enter()
api.fetchPosts { result in
posts = try? result.get()
group.leave()
}
group.notify(queue: .main) {
guard let user, let posts else { /* 错误处理又该放在哪里? */ return }
render(user, posts)
}
}
需要动用 DispatchGroup、临时存储变量和分散的错误处理。当请求增加到三个、四个,并混入“ A 完成后用结果请求 B”这样的依赖关系时,嵌套会变得无法控制。这就是所谓的回调地狱。
在 Combine 中,同样的需求可以这样表达。
api.fetchUser()
.zip(api.fetchPosts())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion { showError(error) }
}, receiveValue: { user, posts in
render(user, posts)
})
.store(in: &cancellables)
“把两者 zip 起来,在主线程接收,成功时通知,失败时也通知。”代码几乎可以直接读出需求,错误处理也集中在一处。
墙 2:控制连续事件
想想搜索框。如果每输入一个字符就调用 API,输入“Swift”五个字符就会发出五次请求。因此通常会加上这些条件。
- 仅在停止输入 0.3 秒后发起请求(debounce)
- 如果与上一次搜索词相同,则不发起请求(去重)
- 发起新请求后,取消尚未完成的旧请求
如果用 Timer 和标志变量直接实现,计时器失效时机与请求取消时机相互交织,很容易产生 bug。在 Combine 中,只需串联经过验证的运算符即可。
searchTextSubject
.debounce(for: .seconds(0.3), scheduler: DispatchQueue.main)
.removeDuplicates()
.map { api.search(query: $0) }
.switchToLatest() // 收到新请求时自动取消上一个请求
.sink { results in render(results) }
.store(in: &cancellables)
关键在于:**不自行实现涉及时间的事件控制逻辑,而是组装经过验证的组件来解决。**这正是响应式编程价值最明显的地方。
MVVM 绑定的标准组件
如上一篇文章所述,MVVM(Model-View-ViewModel)必须具备“ViewModel 变化时 View 自动跟着变化”的绑定才算完整。但 UIKit 没有内置绑定。填补这一空缺的是 RxSwift(RxCocoa),从 iOS 13 开始则由 Combine 接替这一角色。
viewModel.$isLoading
.sink { [weak self] in self?.spinner.isAnimating = $0 }
.store(in: &cancellables)
这就是实际工作中大量招聘信息要求 RxSwift 或 Combine 的原因。在基于 MVVM 的代码库中,绑定层实际上就是用这两个框架之一构建的。
RxSwift 和 Combine 有什么不同?
从概念上看,两者都是响应式编程工具。可以认为 Observable 变成了 Publisher,而 subscribe变成了sink,基本只是名称改变。学会一个后,借助术语对应表就能掌握另一个。不过,选择标准仍然很明确。
- RxSwift:第三方库。没有 iOS 版本限制,借助 RxCocoa 提供丰富的 UIKit 绑定,长期积累的资料和社区是优势。代价是增加外部依赖,并延长构建时间。
- Combine:Apple 的第一方框架。iOS 13 及以上无需添加依赖即可使用,并能自然衔接 SwiftUI 的
@Published、ObservableObject。不过,它对 UIKit 绑定的支持不如 RxCocoa 丰富。
新项目通常可以默认选择 Combine;加入现有代码库已经使用 RxSwift 的团队时,则往往需要学习 RxSwift。
有了 async/await 之后还需要它吗?
Swift 5.5 引入 async/await 后,人们经常问:“现在是不是不用学 Combine 了?”这话只对了一半。对于以 **“一次请求、一次响应”**结束的单次异步任务,async/await 更易读。上面的个人资料示例只需 async let两行。
但像搜索框这样 持续流入的值流,情况就不同了。如果要对文本输入、位置更新、WebSocket 消息或 ViewModel 状态变化等持续事件施加 debounce、combineLatest 等基于时间的控制,仍然属于响应式工具的领域。Apple 也在通过 AsyncSequence 扩展这一领域,但运算符生态目前仍然是 Combine 更成熟。
总结如下。
- 单次异步任务(如网络请求)→ async/await
- 持续事件流 + 基于时间的控制、UI 绑定 → Combine(或 RxSwift)
与其说两者是竞争关系,不如说是负责不同领域的工具。
总结
用一句话概括使用 RxSwift 和 Combine 的原因,就是:将各不相同的异步事件统一为一个流接口,并在其上以声明式方式处理组合、时间控制和绑定。
- 将分散在代理、闭包和通知中的事件处理统一为一种方式
- 无需嵌套回调即可表达多个异步任务的组合
- 用经过验证的运算符解决 debounce、去重和请求取消等时间控制
- MVVM 绑定的事实标准组件
语法初看似乎门槛很高,但只要掌握“把事件看作值流”这一视角,运算符不过是将数组的 map、filter沿时间轴扩展而已。下一篇将深入拆解 Combine 的 Publisher 和 Subscriber 实际如何配合运行。

