Combine 是 Apple 推出的一个框架,用于处理异步事件和响应式编程。它可以帮助开发者以声明性的方式处理复杂的异步任务,例如网络请求、定时器、UI 更新等,并且能避免回调地狱(Callback Hell)。
在 SwiftUI 中,Combine 被大量用来处理数据绑定、网络请求、UI 状态管理等。
核心概念
1、Publisher(发布者)
提供数据流,可以是单个值或多个值。代表事件的来源,比如网络请求结果、用户输入、定时器触发等。
常见的内置 Publisher:
1)Just:发布一个单一值。
Just 是一种特殊的 Publisher,它只发布一个单一的值,然后立即完成。适用于需要立即返回一个简单值的场景。
特点:只能发布一个值,一旦创建,就不可修改,发布后立即完成订阅。
import Combine
let justPublisher = Just(42)
let subscription = justPublisher.sink(
receiveCompletion: { completion in
print("Completion: \(completion)") // 打印 "Completion: finished"
},
receiveValue: { value in
print("Value: \(value)") // 打印 "Value: 42"
}
)
2)PassthroughSubject:动态发布多个值。
它不会保留任何先前发送的值,新的订阅者只会收到订阅之后的值。常用于处理用户交互、通知或其他需要即时传播的事件。
特点:没有初始值,只会发布订阅后产生的值,手动调用 .send(_:) 发布数据。
import Combine
let subject = PassthroughSubject<String, Never>()
let subscription = subject.sink(
receiveCompletion: { completion in
print("Completion: \(completion)")
},
receiveValue: { value in
print("Value: \(value)")
}
)
subject.send("Hello") // 打印 "Value: Hello"
subject.send("World") // 打印 "Value: World"
subject.send(completion: .finished) // 打印 "Completion: finished"
3)CurrentValueSubject:持有一个当前值,并可以发布新的值。
每次订阅时,订阅者都会收到当前值,然后再接收之后的值。常用于状态管理或需要持有某个值的场景。
特点:持有一个初始值,任何订阅者都会先收到当前值,手动调用 .send(_:) 更新值。
import Combine
let currentValueSubject = CurrentValueSubject<Int, Never>(10)
let subscription1 = currentValueSubject.sink(
receiveCompletion: { completion in
print("Completion: \(completion)")
},
receiveValue: { value in
print("Subscription1 Value: \(value)")
}
)
currentValueSubject.send(20) // Subscription1 收到 20
let subscription2 = currentValueSubject.sink(
receiveValue: { value in
print("Subscription2 Value: \(value)")
}
)
// Subscription2 会先收到当前值 20
currentValueSubject.send(30) // 两个订阅者都收到 30
// 输出:
// Subscription1 Value: 10
// Subscription1 Value: 20
// Subscription2 Value: 20
// Subscription1 Value: 30
// Subscription2 Value: 30
4)Swift标准库的Combine扩展
除了上述的三个内置Publisher外,Swift 标准库中还有多个类型被扩展为 Combine 的 Publisher。这些扩展很多是由 Apple 直接在 Combine 框架中提供的,有些是通过 Foundation 框架间接支持的。
常见被扩展为Publisher的类型列表:
1、Array, Set, Dictionary, Range:.publisher方法,变成 Publishers.Sequence。
2、Optional:publisher方法,有值时发出1个值;nil时直接 .finished。
3、NotificationCenter:.publisher(for:)方法,监听系统通知,变成 Publisher。
4、URLSession:.dataTaskPublisher(for:)方法,网络请求直接返回 Publisher。
5、KVO 对象:.publisher(for:)方法,键值观察(KVO)也可以变成 Publisher。
6、Timer:Timer.publish(…)方法,定时器事件流,支持 .autoconnect()。
7、Just / Future / PassthroughSubject / CurrentValueSubject,Combine 原生构造器,已知的 Publisher 类型构建器。
2、Subscriber(订阅者)
订阅 Publisher,接收其发布的数据。
开发者可以实现自己的订阅者,或者使用 Combine 提供的内置方法,例如 .sink 或 .assign。
import Combine
let publisher = Just("Hello, Combine!")
let subscriber = publisher.sink { value in
print("Received value: \(value)")
}
// 输出:Received value: Hello, Combine!
1)sink:它是 Combine 中最常用、最直观的订阅方法之一。它本质上就是一个“订阅者(Subscriber)构造器”。
1、完整的 .sink 示例
let cancellable = somePublisher.sink(
receiveCompletion: { completion in
// 订阅完成(正常完成或失败)时调用
},
receiveValue: { value in
// 每次收到 Publisher 发出的值时调用
}
)
somePublisher 是一个 Combine Publisher,比如 Just(5) 或 PassthroughSubject<Int, Never>()。
.sink() 返回一个 AnyCancellable,用于管理生命周期(取消订阅)。
2、.sink 和订阅者(Subscriber)之间的关系
在 Combine 中:Publisher 只有在被订阅(Subscribe)之后才开始“发出事件”。
.sink 本质上是创建了一个匿名的订阅者(Subscriber),用于接收值和完成事件。
当 .sink 创建了一个订阅者,隐式实现了 Subscriber 协议。在建立连接时,Publisher 发送数据。.sink 接收 value 和 completion,调用传入的闭包。.sink返回返回 AnyCancellable时,可用于取消订阅(释放内存)。
可以理解为:
let subscriber = Subscriber(...) // 隐式构建
publisher.subscribe(subscriber) // 等价于 .sink(...)
3、.sink有两种常用写法:
1)简单写法
sink { value in ... }
2)完整写法
sink(
receiveCompletion: { completion in ... },
receiveValue: { value in ... }
)
这两种写法都合法,第二种是 sink(receiveValue:) 的简写形式,在不关心 .finished 或 .failure 时使用。
.sink(receiveValue:),用于简介监听值,不处理结束或错误。
.sink(receiveCompletion:, receiveValue:),功能更完整,接收 .finished 或 .failure。
4、.sink返回的是什么?
let cancellable: AnyCancellable = ...
这是一个 Combine 提供的类型,用于取消订阅(手动释放资源):
cancellable.cancel()
如果把 cancellable 丢了,Publisher 会照常发,但没人监听它。
5、和SwiftUI的关系
在 SwiftUI 中,推荐使用 .onReceive() 代替 .sink(),因为:
.onReceive():自动管理生命周期(绑定视图)。
.sink():需要手动保留 AnyCancellable,否则被释放了就不再接收事件。
3、Operator(操作符)
用于对数据流进行变换、过滤、合并等操作,类似于 Swift 的 map、filter、reduce。
常用操作符:
map:将一个值映射为另一个值。
filter:筛选满足条件的数据。
combineLatest:合并多个 Publisher 的最新值。
flatMap:将一个 Publisher 的值映射为另一个 Publisher。
import Combine
let publisher = Just(5)
let mappedPublisher = publisher.map { $0 * 2 } // 乘以 2
mappedPublisher.sink { value in
print("Mapped value: \(value)") // 输出:Mapped value: 10
}
4、Cancellable(取消任务)
订阅后返回的对象,可以通过调用 .cancel() 停止订阅,避免内存泄漏。
5、Subjects(主题)
既是 Publisher,也是 Subscriber。
可用于手动触发值的发布。
PassthroughSubject:初始没有值,仅发送接收到的值。
CurrentValueSubject:保存当前值,并向新订阅者发送最新值。
使用场景
1、数据流处理
import Combine
// 1. 创建一个 Publisher
let numbers = [1, 2, 3, 4, 5].publisher
// 2. 使用 Operator 转换数据
let subscription = numbers
.filter { $0 % 2 == 0 } // 只保留偶数
.map { $0 * 10 } // 将每个值乘以 10
.sink { value in
print("Received value: \(value)")
}
// 输出:
// Received value: 20
// Received value: 40
2、Optional 变成 Publisher:
let someValue: Int? = 10
someValue.publisher
.sink { print("Value: \($0)") }
// 输出:Value: 10
let noneValue: Int? = nil
noneValue.publisher
.sink { print("不会有输出") }
// 输出:无(直接完成)
3、Timer发布时间流:
let timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { time in
print("Tick: \(time)")
}
4、网络请求 + SwiftUI
import SwiftUI
import Combine
class APIViewModel: ObservableObject {
@Published var data: String = "Loading..."
private var cancellable: AnyCancellable?
func fetchData() {
let url = URL(string: "https://api.example.com/data")!
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { String(data: $0.data, encoding: .utf8) ?? "No Data" }
.replaceError(with: "Error fetching data")
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.data = $0 }
}
}
struct APIView: View {
@StateObject private var viewModel = APIViewModel()
var body: some View {
VStack {
Text(viewModel.data)
.padding()
Button("Fetch Data") {
viewModel.fetchData()
}
}
.onAppear {
viewModel.fetchData()
}
}
}
与 SwiftUI 集成:@Published 和 @StateObject
在 MVVM 架构中,Combine 常用于连接 ViewModel 和 View。SwiftUI 提供了便捷的 @Published 和 @StateObject,简化了响应式编程。
示例:ViewModel 与 View 的绑定
import SwiftUI
import Combine
// ViewModel
class CounterViewModel: ObservableObject {
@Published var count: Int = 0
func increment() {
count += 1
}
}
// View
struct CounterView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
.font(.largeTitle)
Button("Increment") {
viewModel.increment()
}
.padding()
}
}
}
1、Swift代码:ObservableObject和@Published都来自Combine,底层全部依赖Combine的Publisher机制来驱动UI刷新。
class CounterViewModel: ObservableObject {
@Published var count: Int = 0
}
ObservableObject负责协议,提供对象变更通知。@Published是属性包装器,会自动生成一个 Publisher:$count。
2、SwiftUI视图:@StateObject是SwiftUI的属性包装器:
@StateObject private var viewModel = CounterViewModel()
但它要求传入的对象必须是 ObservableObject,也就是说@StateObject 是 SwiftUI 的入口,但真正监听数据变化、触发视图刷新,是由 Combine 驱动的。
3、Combine:如果不导入Combine,实际上也可以编译:
import SwiftUI
// import Combine 不导入Combine
这是因为,SwiftUI默认已经隐式地依赖了Combine。
@Published、ObservableObject 都来自 Combine,但是只要用 SwiftUI,编译器能自动找到这些引用,所以看起来“没导入也能用”。
优点
1、声明式编程:代码可读性高,逻辑清晰。
2、易于组合:通过操作符轻松处理数据流的复杂逻辑。
3、线程管理:通过 .receive(on:) 控制数据在哪个线程中处理。
总结
从 Swift 5.9(iOS 17)开始,@Observable 取代 ObservableObject,但它目前仍处于实验阶段,并且有以下区别:

在学习Combine框架时,如果比较困难,同时App只支持iOS 17+,那么可以考虑使用@Observable替代@ ObservableObject。
相关文章
1、Swift @Observable属性包装器:https://fangjunyu.com/2024/11/03/swift-observable%e5%b1%9e%e6%80%a7%e5%8c%85%e8%a3%85%e5%99%a8/
2、SwiftUI状态管理机制@Observable和@Environment:https://fangjunyu.com/2024/12/23/swiftui%e7%8a%b6%e6%80%81%e7%ae%a1%e7%90%86%e6%9c%ba%e5%88%b6observable%e5%92%8cenvironment/
3、Swift常见的支持 Publisher 响应式处理的类或场景:https://fangjunyu.com/2024/12/14/swift%e5%b8%b8%e8%a7%81%e7%9a%84%e6%94%af%e6%8c%81-publisher-%e5%93%8d%e5%ba%94%e5%bc%8f%e5%a4%84%e7%90%86%e7%9a%84%e7%b1%bb%e6%88%96%e5%9c%ba%e6%99%af/
4、Swift和Foundation框架创建和管理定时任务的Timer类:https://fangjunyu.com/2024/12/14/swift%e5%92%8cfoundation%e6%a1%86%e6%9e%b6%e5%88%9b%e5%bb%ba%e5%92%8c%e7%ae%a1%e7%90%86%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1%e7%9a%84timer%e7%b1%bb/
5、SwiftUI使用Combine监听并动态管理菜单栏案例:https://fangjunyu.com/2025/07/11/swiftui%e4%bd%bf%e7%94%a8combine%e7%9b%91%e5%90%ac%e5%b9%b6%e5%8a%a8%e6%80%81%e7%ae%a1%e7%90%86%e8%8f%9c%e5%8d%95%e6%a0%8f%e6%a1%88%e4%be%8b/
扩展知识
内置Publisher类型
在文中主要讲了三个内置构造器Just、PassthroughSubject、CurrentValueSubject,还有标准库扩展:.publisher on Array、Optional、NotificationCenter、URLSession 等。
实际上还有更多内置Publisher类型,但它们大多数是“组合或封装型”的,用于更高级场景。
常见Publisher类型总览:
1、Just:发送一个值后结束,用于测试、默认值。
2、Fail:立即失败,用于模拟错误流。
3、Empty:不发送任何值,用于占位流、条件跳过。
4、Deferred:延迟创建 Publisher,按需惰性生成。
5、Future:执行一次异步任务后发出值,用于网络请求、延迟操作。
6、Record:发送固定事件序列,用于测试使用。
7、Publishers.Sequence:来自 .publisher 的数组等,用于批量静态值。
8、Publishers.Once:.publisher from Optional,一次性 optional。
9、Publishers.Share:多个订阅共享同一输出,网络共享。
10、Publishers.Timer:来自 Timer.publish(),用于定时事件流。
11、Publishers.Merge, Zip, CombineLatest:组合多个 Publisher,响应多个数据源。
12、Publishers.HandleEvents, TryMap, 等操作符变种:添加副作用、错误捕获等,用于调试、流程控制。
最小可用Publisher列表:
1、Just:单值快速测试,建议掌握。
2、PassthroughSubject:通知型手动流,建议掌握。
3、CurrentValueSubject:状态型手动流,建议掌握。
4、Future:异步任务封装,建议掌握。
5、Empty, Fail:用于控制流结构,可选了解。
6、Deferred:惰性、按需场景,可选了解。
autoconnect()定时器
autoconnect() 是 Combine 中用于定时器(Timer.publish(…))的快捷方法,它的作用是:自动连接定时器的 Publisher,使其立即开始发出事件,而不需要手动调用 .connect()。
当使用Timer.publish创建ConnectablePublisher时:
let timer = Timer.publish(every: 1.0, on: .main, in: .common)
它返回的不是一个普通的Publisher,而是一个:
Publishers.Autoconnect<Publishers.MakeConnectable<Timer.TimerPublisher>>
也就是说,它是“可连接的 Publisher”——默认情况下不会自动开始发出事件,除非手动调用 .connect()。
如果不使用autoconnect() 时:必须手动 .connect()。
let publisher = Timer.publish(every: 1.0, on: .main, in: .common)
let subscription = publisher
.sink { date in
print("Tick: \(date)")
}
let connection = publisher.connect() // 必须手动启动它
使用 .autoconnect() 简化写法
let subscription = Timer
.publish(every: 1.0, on: .main, in: .common)
.autoconnect() // 自动启动定时器
.sink { date in
print("Tick: \(date)")
}
.receive(on:) 有哪些选项?
这个方法的作用是指定在哪个线程/调度器(Scheduler)上接收事件。常见的几个:
1、RunLoop.main:在主线程(UI 线程)接收事件。UI 更新必须使用这个,否则会崩溃或无效。适用于 macOS/iOS App。
.receive(on: RunLoop.main)
2、DispatchQueue.main
也在主线程,但使用 GCD 的方式调度。和 RunLoop.main 类似,差别非常小,主要取决于用的架构。
.receive(on: DispatchQueue.main)
3、DispatchQueue.global(qos: .background)(或 .utility, .userInitiated 等)
切换到后台线程进行耗时操作(例如下载、计算、文件读写等)。
.receive(on: DispatchQueue.global(qos: .background))
4、OperationQueue
更加细粒度的调度器控制,适用于复杂任务依赖或并发控制。
.receive(on: OperationQueue.main)
5、.subscribe(on:) vs .receive(on:)(额外补充)
.subscribe(on:):指定订阅者的工作(如网络请求、文件读取)在哪个线程执行。
.receive(on:):指定收到数据后回调在哪个线程执行(如 UI 更新)。