Combine — 关键概念、Publisher与响应式编程

作者: IT Sectr 发布日期: 2026-05-03 阅读时间: 11 分钟

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本地响应式框架,包含Publisher、Subscriber、Subject类型以及通过Failure实现的错误类型
  • Publisher 随时间发出事件:Output值,可以成功完成或以Failure错误结束
  • Subscriber 从Publisher接收事件,可以通过Demand请求特定数量的元素
  • 操作符 map、flatMap、filter、combineLatest、zip、debounce在无竞态条件下转换流
  • SwiftUI 通过@Published、ObservableObject和@StateObject使用Combine实现响应式界面更新

什么是Combine?

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的基本概念

Combine基于三个协议:Publisher(发出Output类型的值,可能以Failure类型的错误结束)、Subscriber(接收值,管理Demand——请求元素的数量)、Subscription(代表Publisher-Subscriber连接,具有取消能力)。数据传输通道在subscribe时初始化,在cancel、completion或error时结束。Demand——Combine的独特概念:Subscriber告知Publisher准备好处理多少个元素,在协议级别实现背压。

Publisher和Subscriber:响应式流架构

Publisher — 具有两个关联类型的协议:Output(发出值的类型)和Failure(实现Error的错误类型)。如果流不能以错误结束,则Failure指定为Never——这向Subscriber保证onReceive将仅以Output调用。内置Publisher包括Just(单个值)、Sequence(数组)、URLSession.DataTaskPublisher(网络请求)、NotificationCenter.Publisher和@Published属性包装器。

swift
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和Demand

Subscription — 是代表Publisher和Subscriber之间活动连接的协议。Subscriber在receive(subscription:)方法中接收Subscription,并调用request(_:)来指定Demand:.unlimited(所有值)、.max(N)(有限数量)或.none(暂停)。Demand可以动态变化——Subscriber可以在接收数据过程中增加或减少请求的元素数量。这确保了在Publisher端无需缓冲即可实现背压。

Subject和CurrentValueSubject

Subject — 是结合Publisher和Subscriber的类型。Subject既可以用作Publisher(可以订阅它),也可以同时用作Subscriber(可以向其发送值)。Combine提供两种Subject:PassthroughSubject(不存储状态,仅传递新值)和CurrentValueSubject(存储当前值并将其传递给新订阅者)。Subject对于将命令式代码集成到Combine响应式链中至关重要。

swift
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 / replaceErrorFailure后恢复
Demand管理buffer / collect分组或缓冲

flatMap在Combine中与RxSwift版本有一个重要区别:它接受一个返回具有相同Failure类型的Publisher的闭包,并将嵌套的Publisher展开到主流中。带有maxPublishers: .max(1)的flatMap行为类似switchMap——在新值到达时取消之前的嵌套Publisher。这对于搜索场景至关重要:输入新字符时,之前的HTTP请求会自动取消。

swift
// 使用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和线程管理

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。

swift
// 后台下载 + 主线程上的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。

Combine和SwiftUI:通过ObservableObject集成

ObservableObject——SwiftUI协议,用于发布更改的对象。实现ObservableObject的类可以对属性使用@Published属性包装器,这些属性的更改会自动通知SwiftUI需要重绘。在底层,@Published创建一个Publisher,在wrappedValue更改时通知objectWillChange Publisher。SwiftUI通过@StateObject、@ObservedObject或@EnvironmentObject订阅objectWillChange。

@Published和@StateObject

@Published——在SwiftUI中集成Combine的最常见方式。当@Published属性的值更改时,SwiftUI会更新所有使用此对象的View。@StateObject创建ObservableObject实例并订阅其更改。使用@StateObject创建的View在@Published属性更改时自动重绘。如果对象需要在多个View之间传递,则使用@ObservedObject或@EnvironmentObject。

swift
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 vs RxSwift

CombineRxSwift解决相同的响应式编程任务,但具有根本的架构差异。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仍然是合理的解决方案。

特性CombineRxSwift
开发者Apple(嵌入SDK)ReactiveX(社区)
iOS版本iOS 13+iOS 8+
错误类型Generic Failure(UI使用Never)Error(任意)
UI集成@Published + SwiftUIRxCocoa + UIKit
操作符约100个内置400+操作符
Swift Concurrency通过.values(异步序列)通过桥接库

常见问题

PassthroughSubject和CurrentValueSubject有什么区别?

PassthroughSubject不存储状态——订阅者只接收订阅后发送的事件。CurrentValueSubject存储当前值,并在订阅时立即将其传递给每个新订阅者。CurrentValueSubject适合表示状态(例如isLoggedIn)。

如何在Combine中取消订阅?

订阅返回AnyCancellable,在调用cancel()或去初始化时取消。对于组管理,使用Set<AnyCancellable>——清除set时所有订阅都会被取消。这相当于RxSwift中的DisposeBag。

在async/await之后还需要学习Combine吗?

是的,Combine对于流数据仍然重要:UI事件、debounce、combineLatest、WebSocket。async/await适用于单个请求,Combine——适用于连续或多个流。两个框架互补——Publisher可以转换为AsyncSequence。

如何将Combine与UIKit一起使用?

UIKit没有内置Publisher,但Apple提供扩展:NotificationCenter.default.publisher(for:)、Timer.publish、URLSession.dataTaskPublisher。对于自定义UI事件,使用PassthroughSubject或通过Future或Subject包装到Publisher中的@IBAction。

Combine中的背压是什么?

背压——流速度控制机制:Subscriber通过Demand告知Publisher准备好处理多少个元素。如果Demand = .max(1),Publisher在发送下一个值之前等待请求。这可以防止生产者和消费者速度不匹配时缓冲区溢出。

总结

  • Combine——Apple本地响应式框架,适用于iOS 13+,采用Publisher-Subscriber模式和严格错误类型化
  • PublisherSubscriber形成通信通道,通过Demand管理实现背压
  • Subject(Passthrough和CurrentValue)将命令式代码集成到响应式链中
  • 操作符map、flatMap、combineLatest、debounce、catch提供声明式流处理
  • Scheduler通过DispatchQueue和RunLoop管理执行线程,不阻塞UI
  • SwiftUI通过@Published、ObservableObject和@StateObject使用Combine实现响应式View
  • RxSwift仍然是iOS 8+项目的替代方案,Combine——iOS 13+新项目的选择

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读