Combine 是 Apple 的反应式编程框架,于 iOS 13 中推出。根据 Apple Documentation, 2025,Combine 提供了统一的声明式 API 用于处理异步事件。Publisher 定义数据源,而 Subscriber 订阅以接收数据。
要点
Combine 是一个声明式 Swift 框架,用于随时间处理异步事件。与基于回调的方法不同,Combine 允许通过操作符链来描述数据处理管道。该框架与 Foundation(URLSession、Timer、NotificationCenter)和 SwiftUI(ObservableObject、@Published)集成。
每个实现 Publisher 协议的类型必须定义 Output(值类型)和 Failure(错误类型)。Publisher 在订阅之前不活跃 — 只有在调用 subscribe 后才开始发出事件。这是冷语义,是反应式流的特征,可提高效率。
Subscriber 通过三种方法接收事件:receive(subscription:) — 通过 demand 管理确认订阅;receive(_:) — 接收新值;receive(completion:) — 以成功或错误完成流。Demand 指定 Subscriber 准备接收的值的数量 — 这是背压机制。
Apple 提供了许多内置 Publisher:Just 发出一个值然后结束,Future — 带有闭包的异步结果,Deferred — 延迟创建 Publisher 直到订阅时刻。URLSession.dataTaskPublisher 将网络请求转换为反应式流。Timer.publish 创建周期性计时器。NotificationCenter.default.publisher 将通知转换为 Publisher。这些集成允许在无需手动桥接的情况下订阅事件。
在 Combine 中,Publisher 和 Subscriber 通过 Subscription 协议连接。Subscription 是一个连接对象,控制数据流。开发者无需创建自己的 Publisher 类型 — Apple 提供了内置的:Just、Future、Deferred、Fail、Empty,以及用于 URLSession 和 Timer 的 Publisher。
Subject 是一个可以手动发送值的 Publisher。PassthroughSubject 不存储状态:订阅者只接收订阅后发生的事件。CurrentValueSubject 类似于 RxSwift 中的 BehaviorRelay,存储当前值并立即将其传递给新订阅者。
Combine 中的 背压 机制调节数据传输速度。Subscriber 通过 subscription.request(.unlimited) 或 subscription.request(.max(N)) 告知 Publisher 它准备处理多少值。这可以防止在生产者和消费者速度不均衡时缓冲区溢出。
Combine 包含超过 100 个操作符,分为以下类别:转换(map、tryMap、flatMap)、过滤(filter、compactMap、removeDuplicates)、组合(combineLatest、merge、zip)、时间管理(debounce、throttle、delay)和错误处理(catch、replaceError、retry)。
combineLatest 操作符组合两个 Publisher 的最新值 — 每当其中一个发出新值时,闭包都会以两个更新后的值被调用。该操作符在表单验证中不可或缺,需要同时跟踪多个输入字段的状态。
debounce 操作符延迟值的发布,直到经过指定间隔而没有新事件。这对搜索字段至关重要:只有在用户停止输入 300-500 毫秒后才向服务器发送请求,从而将负载降低 5-10 倍。
Combine 在操作符级别提供多种错误处理策略。catch 拦截 Failure 并用备用 Publisher 替换 — 流继续而不会崩溃。replaceError(with:) 将默认值放在错误的位置。retry(_:) 在失败时重复订阅指定次数。重要:Publisher 签名中的 Never 错误类型保证流永远不会以错误结束 — 这允许在 SwiftUI 中使用 Publisher 而无需处理 Failure。
第一个示例展示了 Just 的基本工作 — Publisher 发出一个值并结束:
let publisher = Just("Hello Combine")
publisher
.sink(receiveCompletion: { completion in
print("完成: \(completion)")
}, receiveValue: { value in
print("收到: \(value)")
})
第二个示例演示了 Combine 与 URLSession 的集成,用于网络请求并在主线程上处理结果:
let url = URL(string: "https://api.example.com")!
URLSession.shared
.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("错误: \(error)")
}
}, receiveValue: { data in
print("数据收到: \(data.count)")
})
第三个示例 — 使用 CurrentValueSubject 进行状态管理:
let counter = CurrentValueSubject<Int, Never>(0)
counter
.sink { value in
print("计数器: \(value)")
}
counter.send(1) // 显示: 计数器: 1
counter.send(2) // 显示: 计数器: 2
print(counter.value) // 显示: 2
Combine 和 RxSwift 解决相同的任务 — 反应式编程,但方法不同。Combine 是 Apple 的原生框架,内置于系统中:不需要安装依赖,与 SwiftUI 和 Foundation 集成。RxSwift 是一个第三方库,支持 iOS 9+ 并拥有更广泛的操作符集。
Combine 的性能在典型场景中更高,这得益于用 C 语言实现的原生代码。RxSwift 提供更多调试工具(RxSwift.Resources.total、Debug),但其面向对象的架构在大数据流上可能效率较低。两者之间的选择是功能与集成之间的权衡。
一个重要区别 — 背压模型。在 Combine 中,Subscriber 通过 Subscription 管理 demand,从而对流的速度进行显式控制。在 RxSwift 中,背压默认不受控制 — Observable 发出所有值,Subscriber 在到达时处理它们。对于大数据流,Combine 更可预测且在内存方面更安全。
Combine 在语言级别与 SwiftUI 深度集成。@Published 生成 Publisher,ObservableObject 提供 objectWillChange Publisher。SwiftUI View 自动订阅这些 Publisher 并在更改时重新绘制。对于 UIKit,存在第三方包装器如 CombineCocoa,但 Combine 最初是为 SwiftUI 设计的,因此其在 UIKit 中的集成需要额外代码。相比之下,RxSwift 拥有丰富的 UIKit 生态系统:RxCocoa 为所有 UI 元素提供反应式包装器。
Combine 支持通过 NSFetchedResultsController Publisher 与 Core Data 集成。Publisher 在 Core Data 堆栈每次更改时发出新数据:添加、删除、更新记录。这允许构建反应式列表而无需手动调用 reloadData。通过将 Combine 与 UIApplication.willResignActiveNotification 的 NotificationCenter Publisher 结合,开发人员无需委托和回调即可为整个应用程序生命周期创建反应式层。对于 ObservableObject 类,SwiftUI 在 @Published 属性更改时自动更新 View — Combine 提供了整个连接,无需一行手动代码。Combine 代码的模块化测试通过 XCTestExpectation 和 Publisher.sink 在测试用例中进行,通过 ImmediateScheduler 实现 Scheduler 隔离。
在 UIKit 项目中使用 Combine 时,开发人员通常通过 sink 使用订阅并将 AnyCancellable 存储在 Set 中。典型模式:ViewController 创建视图,存储 cancellables,订阅来自 ViewModel 的 Publisher。在 deinit 时,所有订阅通过 Cancellables 集合自动取消。为了在 Combine 中正确使用 UIControl,可以通过 publisher(for:) 扩展使用 UIControl.Event Publisher。
Combine 的调试通过 print() 或 handleEvents() 操作符完成。print(String) 记录所有事件:receive subscription、request demand、receive value、receive completion。handleEvents 提供更精细的控制:可以为 Publisher 生命周期的每个阶段指定闭包。对于 Combine 测试,存在 CombineExpectations — 一个允许检查 Publisher 在具有虚拟时间和 demand 控制的测试中是否发出预期值的库。
Combine 在 SwiftUI 中积极用于表单管理:每个 @Published 属性生成一个 Publisher,SwiftUI 通过 View.body 订阅它。通过 combineLatest 组合多个 Publisher,表单验证变得声明式:每个 Publisher 跟踪一个字段,combineLatest 收集所有值,map 检查它们并返回 Submit 按钮的状态。Publishers.Merge 将多个相同类型的 Publisher 合并为一个 — 用于将来自不同控件的事件收集到单个流中。
要将 Combine 与 UIKit 集成,请通过扩展使用 UIControl Publisher:button.publisher(for: .touchUpInside) 返回一个在按下时发出事件的 Publisher。CombineCocoa — 一个社区库,为所有 UIControl 事件、UITextView.text、UIScrollView.contentOffset 提供 Publisher。取消订阅时,cancellables Store 在 deinit 中被清除,确保没有订阅比其所有者存活更久。对于多状态的 UIControl,使用 Publishers.MergeMany 将 Publisher 组合到数组中。
常见问题
Combine 从 iOS 13、macOS 10.15、tvOS 13 和 watchOS 6 开始可用。这限制了它在需要支持旧版本的项目中的使用。对于 iOS 12 及以下版本,使用 RxSwift 或第三方库。
PassthroughSubject 不存储状态,也不向新订阅者重复最后一个值 — 他们只接收未来事件。CurrentValueSubject 存储当前值并立即将其传递给新订阅者。CurrentValueSubject.value 允许同步读写当前值。
Combine 是 SwiftUI 的基础:@Published 生成 Publisher,ObservableObject 使用 objectWillChange Publisher。SwiftUI View 通过 Combine 管道自动订阅 @ObservedObject 和 @StateObject,在属性更改时重新绘制 View。
背压 — 数据传输速度管理机制。Subscriber 告知 Subscription 它准备处理多少值(demand)。Publisher 不能发出超过请求的数量。这在处理来自网络套接字的大数据流时至关重要。
catch 操作符捕获错误并用后备 Publisher 替换。replaceError 用默认值替换错误。retry 在错误时重复订阅指定次数。所有操作符应放置在 subscribe 之前,以防止在 Failure 时崩溃。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。