Combine — 是Apple的本地响应式编程框架,在iOS 13、macOS Catalina、tvOS 13和watchOS 6中引入。它通过Publisher和Subscriber模式提供声明式Swift API来处理异步事件,用统一链替代委托、闭包和NotificationCenter。根据Apple, 2025,Combine是SwiftUI和现代iOS架构的基础,与async/await和Structured Concurrency紧密配合。该框架专为异步操作的组合设计,具有线程安全保障。
要点
Combine — 是嵌入Apple SDK的声明式响应式编程框架。它实现了Reactive Streams模式:Publisher生成值,Subscriber消费它们,操作符在它们之间转换流。Combine解决了回调和委托的问题,为任何异步事件(从网络响应到UI更改)提供统一的组合模型。
在Combine出现之前,iOS开发者使用第三方库,主要是RxSwift。Apple创建Combine作为本机替代方案,深度集成到生态系统中:该框架通过@objc桥支持Objective-C,与KVO(通过NSObject.keyValuePublisher进行键值观察)和NotificationCenter配合使用,并且也是SwiftUI的基础。SwiftUI中发布的所有UIKit组件都在底层使用Combine来更新视图。
Combine在设计时考虑了Swift Concurrency:从iOS 15开始,Publisher可以通过.values转换为AsyncSequence,并在for-await-in循环中使用。异步函数到Publisher的反向转换通过Future完成。根据Apple WWDC 2024,尽管出现了用于单个异步调用的async/await,Combine仍然是UIKit应用程序中处理流数据的推荐框架。
Combine基于三个协议:Publisher(发出Output类型的值,可能以Failure类型的错误结束)、Subscriber(接收值,管理Demand——请求元素的数量)、Subscription(代表Publisher-Subscriber连接,具有取消能力)。数据传输通道在subscribe时初始化,在cancel、completion或error时结束。Demand——Combine的独特概念:Subscriber告知Publisher准备好处理多少个元素,在协议级别实现背压。
Publisher — 具有两个关联类型的协议:Output(发出值的类型)和Failure(实现Error的错误类型)。如果流不能以错误结束,则Failure指定为Never——这向Subscriber保证onReceive将仅以Output调用。内置Publisher包括Just(单个值)、Sequence(数组)、URLSession.DataTaskPublisher(网络请求)、NotificationCenter.Publisher和@Published属性包装器。
import Combine
// 从序列创建Publisher
let publisher = [1, 2, 3, 4, 5].publisher
// 创建带有值处理的Subscriber
class PrintSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>) {
print("已完成")
}
}
publisher.subscribe(PrintSubscriber())
Subscription — 是代表Publisher和Subscriber之间活动连接的协议。Subscriber在receive(subscription:)方法中接收Subscription,并调用request(_:)来指定Demand:.unlimited(所有值)、.max(N)(有限数量)或.none(暂停)。Demand可以动态变化——Subscriber可以在接收数据过程中增加或减少请求的元素数量。这确保了在Publisher端无需缓冲即可实现背压。
Subject — 是结合Publisher和Subscriber的类型。Subject既可以用作Publisher(可以订阅它),也可以同时用作Subscriber(可以向其发送值)。Combine提供两种Subject:PassthroughSubject(不存储状态,仅传递新值)和CurrentValueSubject(存储当前值并将其传递给新订阅者)。Subject对于将命令式代码集成到Combine响应式链中至关重要。
let subject = PassthroughSubject<String, Never>()
// 作为Publisher订阅
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// 作为Subscriber发送值
subject.send("hello") // 打印“HELLO”
subject.send("世界") // 打印“WORLD”
CurrentValueSubject与PassthroughSubject的区别在于具有初始值和value属性:订阅者在订阅时立即接收当前值,然后接收所有后续更新。CurrentValueSubject.value可读可写——更改value会自动向所有订阅者发送新值。这使得CurrentValueSubject成为在MVVM架构中表示状态的理想选择:ViewModel发布CurrentValueSubject,View通过sink订阅更改。
两种Subject都可以通过调用send(completion: .finished)或send(completion: .failure(error))来结束流。完成后,Subject停止接收和传递事件。对于不应结束的长期流(例如UI事件),建议使用带有Never Failure的PassthroughSubject,以排除意外调用send(completion:)。
操作符 Combine — 是返回新Publisher的Publisher方法。每个操作符创建一个新对象,订阅上游Publisher并向下游发出转换后的值。由于Publisher是泛型类型,操作符保持严格类型化:map将Output<A>转换为Output<B>,tryMap添加错误可能性。Combine包含大约100个内置操作符。
| 类别 | 操作符 | 用途 |
|---|---|---|
| 转换 | map / tryMap / flatMap | 转换值或流 |
| 过滤 | filter / compactMap / removeDuplicates | 选择或清理值 |
| 组合 | combineLatest / zip / merge | 合并多个Publisher |
| 时间管理 | debounce / throttle / delay | 延迟和稀释事件 |
| 错误处理 | catch / retry / replaceError | Failure后恢复 |
| Demand管理 | buffer / collect | 分组或缓冲 |
flatMap在Combine中与RxSwift版本有一个重要区别:它接受一个返回具有相同Failure类型的Publisher的闭包,并将嵌套的Publisher展开到主流中。带有maxPublishers: .max(1)的flatMap行为类似switchMap——在新值到达时取消之前的嵌套Publisher。这对于搜索场景至关重要:输入新字符时,之前的HTTP请求会自动取消。
// 使用debounce搜索并取消之前的请求
searchTextField.textPublisher
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap(maxPublishers: .max(1)) { query in
apiService.searchPublisher(query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.sink { results in
self.tableView.reloadData()
}
.store(in: &cancellables)
组合操作符——combineLatest和zip——工作方式类似于RxSwift:combineLatest在任一Publisher更改时发出所有Publisher最后值的元组;zip按索引成对组合值。merge将相同类型的Publisher合并为一个流,不保证preserveOrder。Combine中还有select——一个罕见的操作符,从多个中选择第一个完成的Publisher,以及share——将流多播给多个订阅者而无需重复执行。
Scheduler在Combine中——是定义操作符执行上下文的协议。与RxSwift有5+内置Scheduler不同,Combine使用现有的Apple机制:DispatchQueue、RunLoop和OperationQueue。这些类型各自符合Scheduler协议,允许直接传递给receive(on:)和subscribe(on:),无需额外适配器。
receive(on:)将下游切换到指定的Scheduler——相当于RxSwift中的observeOn。receive(on:)之后的所有操作符在指定的Scheduler上执行。subscribe(on:)切换上游——影响Publisher的执行。典型模式:subscribe(on: DispatchQueue.global())用于后台工作,receive(on: DispatchQueue.main)用于UI更新。在SwiftUI中使用.onReceive时,不需要与主线程的内置绑定,但对于sink,建议显式使用receive(on:).main。
// 后台下载 + 主线程上的UI
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.receive(on: DispatchQueue.main)
.decode(type: User.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print($0) },
receiveValue: { self.nameLabel.text = $0.name })
.store(in: &cancellables)
RunLoop.main——DispatchQueue.main的替代方案,用于UI操作。区别在于RunLoop.main绑定到应用程序的当前事件循环,而DispatchQueue.main绑定到主线程的全局队列。对于UIKit建议使用DispatchQueue.main,对于SwiftUI——RunLoop.main。ImmediateWhenScheduler在当前线程上同步执行操作——默认用于测试和简单Publisher。
ObservableObject——SwiftUI协议,用于发布更改的对象。实现ObservableObject的类可以对属性使用@Published属性包装器,这些属性的更改会自动通知SwiftUI需要重绘。在底层,@Published创建一个Publisher,在wrappedValue更改时通知objectWillChange Publisher。SwiftUI通过@StateObject、@ObservedObject或@EnvironmentObject订阅objectWillChange。
@Published——在SwiftUI中集成Combine的最常见方式。当@Published属性的值更改时,SwiftUI会更新所有使用此对象的View。@StateObject创建ObservableObject实例并订阅其更改。使用@StateObject创建的View在@Published属性更改时自动重绘。如果对象需要在多个View之间传递,则使用@ObservedObject或@EnvironmentObject。
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
private var cancellables = Set<AnyCancellable>()
init() {
$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] newName in
AnalyticsService.logNameChange(newName)
}
.store(in: &cancellables)
}
}
struct UserView: View {
@StateObject var viewModel = UserViewModel()
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
AnyCancellable——Cancellable的类型擦除器,存储订阅的取消令牌。Set<AnyCancellable>管理订阅的生命周期:所有者去初始化时,所有Cancellable会自动取消。在SwiftUI项目中,Set<AnyCancellable>在ObservableObject类中声明,订阅通过.store(in: &cancellables)添加。对于UIKit,使用相同的机制,在UIViewController中通过&cancellables存储或手动调用cancel()。
Combine和RxSwift解决相同的响应式编程任务,但具有根本的架构差异。Combine——Apple SDK的一部分,向后兼容到iOS 13,RxSwift——第三方库,支持iOS 8+。Combine通过Failure泛型使用严格错误类型化,RxSwift——统一的Error类型。Combine在平台级别与SwiftUI集成,RxSwift需要RxCocoa进行UI扩展。
Combine和RxSwift之间的选择取决于项目需求。如果最低iOS版本>=13且项目使用SwiftUI——Combine因内置集成和无额外依赖而成为自然选择。如果项目支持iOS 11-12,包含现有RxSwift代码库或需要仅在RxSwift中可用的特定操作符(例如Observable.from(path:)),——RxSwift仍然是合理的解决方案。
| 特性 | Combine | RxSwift |
|---|---|---|
| 开发者 | Apple(嵌入SDK) | ReactiveX(社区) |
| iOS版本 | iOS 13+ | iOS 8+ |
| 错误类型 | Generic Failure(UI使用Never) | Error(任意) |
| UI集成 | @Published + SwiftUI | RxCocoa + UIKit |
| 操作符 | 约100个内置 | 400+操作符 |
| Swift Concurrency | 通过.values(异步序列) | 通过桥接库 |
常见问题
PassthroughSubject不存储状态——订阅者只接收订阅后发送的事件。CurrentValueSubject存储当前值,并在订阅时立即将其传递给每个新订阅者。CurrentValueSubject适合表示状态(例如isLoggedIn)。
订阅返回AnyCancellable,在调用cancel()或去初始化时取消。对于组管理,使用Set<AnyCancellable>——清除set时所有订阅都会被取消。这相当于RxSwift中的DisposeBag。
是的,Combine对于流数据仍然重要:UI事件、debounce、combineLatest、WebSocket。async/await适用于单个请求,Combine——适用于连续或多个流。两个框架互补——Publisher可以转换为AsyncSequence。
UIKit没有内置Publisher,但Apple提供扩展:NotificationCenter.default.publisher(for:)、Timer.publish、URLSession.dataTaskPublisher。对于自定义UI事件,使用PassthroughSubject或通过Future或Subject包装到Publisher中的@IBAction。
背压——流速度控制机制:Subscriber通过Demand告知Publisher准备好处理多少个元素。如果Demand = .max(1),Publisher在发送下一个值之前等待请求。这可以防止生产者和消费者速度不匹配时缓冲区溢出。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。