你是否厌倦了层层嵌套的闭包回调?是否在寻找一种优雅的方式来管理异步数据流?2019 年,Apple 在发布 SwiftUI 的同时,悄然推出了 Combine 框架------这是 Apple 官方出品的响应式编程框架,也是 SwiftUI 数据驱动的"幕后引擎"。掌握 Combine,你将能以声明式的方式编写出简洁、健壮且高度可测试的代码。
1. Combine 框架概述
1.1 什么是 Combine?
Combine 是 Apple 在 WWDC 2019 上随 iOS 13 引入的响应式编程框架,它为 Swift 生态系统提供了一套声明式 API,专门用于处理随时间变化的异步事件流。Combine 的核心思想可以浓缩为一句话:声明发布者暴露随时间变化的值,声明订阅者从发布者接收这些值。
Combine 框架声明了一个 Publisher 协议,它能够随时间传递一系列值。发布者拥有操作符,可以对从上游接收的值进行处理并重新发布。Subscriber 协议则声明了能够接收发布者输入的类型。
1.2 Combine 的核心三件套:Publisher → Operator → Subscriber
Combine 的数据流结构非常清晰,由三个核心角色组成一个完整的处理链条:
发布者] -->|发送值| B[Operator
操作符] B -->|转换后的值| C[Subscriber
订阅者] A -.->|Completion/Error| C style A fill:#007AFF,color:#fff,stroke:none style B fill:#FF9500,color:#fff,stroke:none style C fill:#34C759,color:#fff,stroke:none
这三个角色的职责如下:
-
Publisher(发布者) :负责产生和传输数据到订阅者。它定义了产生的数据类型(Output)和可能的错误类型(Failure)。当发布者保证不会产生错误时,使用
Never类型声明。 -
Operator(操作符) :用于转换、过滤和组合发布者。操作符接收上游发布者的值,经过处理后重新发布给下游。
-
Subscriber(订阅者) :接收发布者发送的数据。它通过调用
subscribe(_:)方法订阅发布者来启动数据流。
1.3 Publisher 与 Subscriber 的通信协议
发布者与订阅者之间的通信不是简单的"发-收",而是一个精密的协议链条:
关键步骤说明:
-
subscribe(_:)方法 :订阅者调用此方法将自己附加到发布者。此方法的实现必须 调用receive(subscriber:)来建立发布者与订阅者之间的连接。 -
连接建立后 :订阅者通过
request(_:)方法向发布者传达其数据需求,可以指定最大接收数量或无限接收。 -
背压管理 :
subscription.request机制实现了背压管理,允许订阅者控制发布者的数据传输速率,防止订阅者被过量的数据压垮。
1.4 Combine 的优势
- 声明式编程:只需描述"想要什么",而不是"如何实现"
- 组合性:操作符可以灵活组合成复杂的处理逻辑
- 内置错误处理:提供统一的错误传递和管理机制
- 内存管理 :通过
AnyCancellable自动管理订阅的生命周期 - 类型安全:发布者的 Output 和 Failure 类型在编译时即被确定
2. 发布者(Publisher)
2.1 什么是发布者?
发布者是 Combine 框架的核心,定义一个能够随时间产生一系列值的协议。它的两个关联类型决定了它能发布什么:
Output:发布者产生的值的类型Failure:发布者可能产生的错误类型。如果永远不会失败,则为Never
发布者可以向订阅者发送三种事件:值事件 (包含实际数据)、完成事件 (表示序列正常结束)和错误事件(表示发生错误)。
通常不必自己实现 Publisher 协议,Combine 框架已经内置了多种发布者类型供你直接使用。
2.2 内置发布者一览
以下是 Combine 中最常用的内置发布者:
| 发布者类型 | 描述 | 典型场景 |
|---|---|---|
Just |
只发送一个值,然后立即完成 | 默认值、测试 |
Future |
异步执行,最终只产生一个值或失败 | 单个网络请求 |
Fail |
立即发送错误并终止 | 错误测试 |
Empty |
不发送任何值,可选择立即完成 | 占位符 |
Sequence |
将任何 Sequence 转为发布者 | 遍历数组 |
Timer |
按时间间隔周期性发送值 | 轮询、倒计时 |
PassthroughSubject |
可手动发送值,不保存状态 | 事件总线 |
CurrentValueSubject |
可手动发送值,保存最新值 | 状态管理器 |
2.3 各类发布者详解
2.3.1 Just
Just 是最简单的发布者------只发送一个值,然后立即完成:
swift
import Combine
let publisher = Just("Hello, Combine!")
publisher
.sink { completion in
print("完成: \(completion)")
} receiveValue: { value in
print("收到值: \(value)")
}
// 输出:
// 收到值: Hello, Combine!
// 完成: finished
适用场景:单元测试中的模拟数据、为操作符链提供默认值、将同步值融入 Combine 管道。
2.3.2 Sequence(序列发布者)
任何遵循 Sequence 协议的类型(如数组、范围)都可以通过 .publisher 属性转换为发布者:
swift
let numbers = [1, 2, 3, 4, 5].publisher
numbers
.sink { completion in
print("完成: \(completion)")
} receiveValue: { value in
print("数字: \(value)")
}
// 输出: 1, 2, 3, 4, 5, 完成
2.3.3 Future
Future 用于表示一个将来会完成的异步操作,最终发送一个值或一个错误,适用于替换传统的 completion handler 闭包:
swift
let future = Future<String, Error> { promise in
// 模拟异步耗时操作
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
promise(.success("异步操作完成"))
}
}
future
.sink(
receiveCompletion: { completion in
print("完成: \(completion)")
},
receiveValue: { value in
print("结果: \(value)")
}
)
重要提示 :Future 在初始化时立即执行其闭包,而不是在被订阅时才执行。这是与 Combine 其他发布者不同的行为特性,务必注意。
2.3.4 Fail
Fail 会立即发送一个错误事件,主要用于测试错误处理逻辑:
swift
let errorPublisher = Fail<String, Error>(
error: NSError(
domain: "com.example",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "网络连接失败"]
)
)
errorPublisher
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("收到错误: \(error.localizedDescription)")
}
},
receiveValue: { value in
print("值: \(value)") // 永远不会被调用
}
)
2.3.5 Empty
Empty 是一个特殊的发布者,它不发送任何值,可以选择立即完成或不完成:
swift
// 立即完成的 Empty
Empty<String, Never>()
.sink { completion in
print("完成: \(completion)") // 立即打印 finished
} receiveValue: { value in
print("值: \(value)") // 永远不会被调用
}
// 永不完成的 Empty(可用于超时等场景)
Empty<String, Never>(completeImmediately: false)
2.3.6 Timer(定时器发布者)
Timer.publish 可以创建按时间间隔周期性发送值的发布者:
swift
import Combine
// init 参数:(every: 间隔, tolerance: 容差, on: RunLoop/Queue, in: mode, options: 可选项)
let timer = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect() // 自动连接
let cancellable = timer
.sink { time in
print("当前时间: \(time)")
}
// 使用 DispatchQueue 调度
let queueTimer = Timer.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
.receive(on: DispatchQueue.main)
⚠️ 注意 :Timer.publish 如果不使用 .autoconnect(),则不会自动启动;需要使用 connect() 手动开启。另外,classfunc 可设置的容差参数 tolerance 可以帮助系统优化能效。
2.3.7 PassthroughSubject
PassthroughSubject 是一个可以手动发送值 的发布者,只传递最新值,不保存历史:通过 send() 方法,你可以在任何地方向订阅者推送新值。
swift
let subject = PassthroughSubject<String, Never>()
let cancellable = subject
.sink { completion in
print("完成: \(completion)")
} receiveValue: { value in
print("收到: \(value)")
}
subject.send("你好")
subject.send("Combine")
subject.send(completion: .finished) // 必须使用 subject.send(completion: .finished)
// 输出:
// 收到: 你好
// 收到: Combine
// 完成: finished
2.3.8 CurrentValueSubject
CurrentValueSubject 在功能上与 PassthroughSubject 类似,但保存并暴露当前最新值 :你必须为它提供一个初始值,且随时可以通过 .value 属性读取当前值。该值也会发送给新订阅的订阅者。
swift
let subject = CurrentValueSubject<String, Never>("初始值")
print("订阅前的值: \(subject.value)") // "初始值"
let cancellable1 = subject
.sink { value in
print("订阅者1 收到: \(value)")
}
// 订阅者1 立即收到: 初始值
subject.send("新值")
// 订阅者1 收到: 新值
let cancellable2 = subject
.sink { value in
print("订阅者2 收到: \(value)")
}
// 订阅者2 立即收到最新的值: 新值
2.3.9 Subject 选取指南
| 特性和环境 | PassthroughSubject | CurrentValueSubject |
|---|---|---|
| 状态保存 | 不保存任何值 | 保存最新值 |
| 新订阅者 | 只收到订阅之后的值 | 立即收到最新值 |
| 初始值要求 | 无需初始值 | 必须提供初始值 |
| 典型场景 | 事件通知、用户操作(按钮点击、手势) | 状态管理、开关状态 |
| 内存占用 | 较轻 | 持有当前值 |
3. 订阅者(Subscriber)
3.1 什么是订阅者?
订阅者是接收发布者发送的值和事件的对象。Combine 提供了两个内置的订阅者:
sink--- 通用的订阅者,通过闭包处理值和完成事件assign--- 将值直接分配给对象的属性(通过 KeyPath)
3.2 Sink 订阅者
sink 是最常用的订阅者,提供两个闭包分别处理完成事件和值:
swift
let publisher = [1, 2, 3, 4, 5].publisher
publisher
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("序列正常完成")
case .failure(let error):
print("错误: \(error)")
}
},
receiveValue: { value in
print("值: \(value)")
}
)
简化版本 (当发布者的 Failure 类型为 Never 时):
swift
let safePublisher = Just("不会出错")
safePublisher
.sink { value in
print("值: \(value)")
}
3.3 Assign 订阅者
assign 使用 KeyPath 将值直接分配给对象的属性:
swift
import SwiftUI
class ViewModel: ObservableObject {
@Published var text = ""
}
let viewModel = ViewModel()
let publisher = Just("Hello!")
// 将发布者的值分配给 viewModel 的 text 属性
let cancellable = publisher.assign(to: \.text, on: viewModel)
print(viewModel.text) // "Hello!"
注意事项:
assign不会产生错误(Failure 必须为Never)assign(to:on:)会导致强引用,如果不在合适时机 cancel 会阻止对象释放- SwiftUI 中的
assign(to:)(不带 on 参数)配合@Published使用时,会自动管理内存
4. 内存管理
4.1 AnyCancellable 的核心机制
在 Combine 中,每次调用 sink 或 assign 都会返回一个 AnyCancellable 实例。你必须持有这个实例,否则订阅会立即被取消。
AnyCancellable 代表订阅的生命周期。当它被释放时,关联的订阅会自动取消,确保资源在不再需要时被释放。
4.2 标准模式:Set<AnyCancellable>
最佳实践是使用 Set<AnyCancellable> 统一管理所有订阅:
swift
import SwiftUI
import Combine
class ViewModel: ObservableObject {
@Published var count = 0
private var cancellables = Set<AnyCancellable>()
init() {
// 使用 .store(in:) 将订阅存入集合
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.count += 1
}
.store(in: &cancellables) // 关键一步!
}
// 当 ViewModel 被释放时,cancellables 也被释放
// 所有订阅自动取消
}
为什么使用 Set<AnyCancellable>?
- 简化管理:将多个 AnyCancellable 集中存储,无需手动跟踪每个订阅
- 避免内存泄漏:当 Set 被释放时,所有 AnyCancellable 对象也被释放,订阅自动取消
- 提高可读性 :使用
.store(in:)清晰表达订阅归属
4.3 防止循环引用
Combine 与闭包搭配时,极易产生循环引用。始终使用 [weak self] 打破循环:
swift
// ❌ 错误:强引用 self
publisher
.sink { value in
self.handleValue(value) // 循环引用风险!
}
.store(in: &cancellables)
// ✅ 正确:使用 weak self
publisher
.sink { [weak self] value in
self?.handleValue(value) // 安全
}
.store(in: &cancellables)
4.4 手动取消与解除存储
swift
// 为单个订阅存储引用
let cancellable = publisher.sink { value in
print(value)
}
// 当不再需要时手动取消
cancellable.cancel()
// 或从 Set 中移除特定订阅
// .store(in:) 返回的 AnyCancellable 会自动管理
特殊情况 :如果某个订阅只需要接收第一个值后立即取消,建议在 sink 闭包内主动调用 cancel(),避免订阅悬空:
swift
var cancellable: AnyCancellable?
cancellable = publisher
.sink { [weak cancellable] value in
print("仅首次: \(value)")
cancellable?.cancel() // 收到第一个值后立即取消
}
5. 错误处理
5.1 理解 Combine 的错误类型
在 Combine 中,发布者的 Failure 是编译时确定的关联类型。错误处理操作符本质上是对该类型的"转换"或"吸收":若干发布者可通过一系列操作符将 Failure 从具体错误类型变为 Never,或反之。
| 操作符 | 用途 | 示例 |
|---|---|---|
tryMap |
在转换中抛出错误 | 验证数据 |
mapError |
转换错误类型 | 统一错误格式 |
catch |
捕获错误并返回备用发布者 | 降级服务 |
replaceError(with:) |
用默认值替换错误 | 提供占位内容 |
retry(_:) |
失败后重试 | 网络请求重试 |
assertNoFailure() |
断言不产生错误(仅调试) | Fail 转 Never |
5.2 错误处理操作符详解
tryMap
tryMap 与 map 类似,但允许闭包抛出错误:
swift
let numbers = [1, 2, -3, 4].publisher
numbers
.tryMap { number -> Int in
if number < 0 {
throw NSError(
domain: "com.example",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "负数不允许"]
)
}
return number * 2
}
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("捕获错误: \(error.localizedDescription)")
}
},
receiveValue: { value in
print("值: \(value)")
}
)
// 输出:
// 值: 2
// 值: 4
// 捕获错误: 负数不允许
catch
catch 捕获上游错误并返回一个新的发布者 (通常是一个 Just 或 Empty),实现错误降级:
swift
let subject = PassthroughSubject<String, Error>()
subject
.catch { error -> Just<String> in
print("发生错误,使用默认值: \(error.localizedDescription)")
return Just("默认值")
}
.sink { value in
print("收到: \(value)")
}
subject.send("第一条消息")
subject.send(completion: .failure(
NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "网络中断"])
))
// 输出:
// 收到: 第一条消息
// 发生错误,使用默认值: 网络中断
// 收到: 默认值
retry
retry 在发生错误时重新订阅发布者,最多重试指定次数:
swift
var attemptCount = 0
let flakyPublisher = Future<Int, Error> { promise in
attemptCount += 1
if attemptCount < 3 {
promise(.failure(
NSError(domain: "Retry", code: attemptCount)
))
} else {
promise(.success(42))
}
}
flakyPublisher
.retry(3) // 最多重试 3 次
.sink(
receiveCompletion: { completion in
print("最终完成: \(completion)")
},
receiveValue: { value in
print("最终值: \(value)")
}
)
// 输出:
// 最终值: 42
// 最终完成: finished
5.3 错误处理策略选择
决策原则:
- 可恢复的错误 (如网络超时)→ 使用
retry或catch提供降级方案 - 业务逻辑错误 (如输入无效)→ 使用
tryMap抛出明确错误,在sink中处理 - 不可恢复的错误 → 让错误穿透到
receiveCompletion中统一展示
6. 线程调度
6.1 receive(on:) vs subscribe(on:)
Combine 提供两个关键方法控制代码执行的线程:
| 方法 | 作用 | 使用时机 |
|---|---|---|
subscribe(on:) |
指定订阅和发布发生的调度器 | 耗时操作放到后台 |
receive(on:) |
指定接收值和完成事件的调度器 | UI 更新切回主线程 |
6.2 实战示例
swift
let future = Future<String, Error> { promise in
print("执行在: \(Thread.current)")
// 模拟耗时操作
Thread.sleep(forTimeInterval: 2)
promise(.success("后台结果"))
}
future
.subscribe(on: DispatchQueue.global(qos: .background)) // 后台执行
.receive(on: DispatchQueue.main) // 主线程接收
.sink(
receiveCompletion: { completion in
print("UI 线程处理完成: \(Thread.isMainThread)")
},
receiveValue: { value in
print("UI 更新: \(value), 主线程: \(Thread.isMainThread)")
}
)
6.3 常见调度器
| 调度器 | 描述 | 典型场景 |
|---|---|---|
DispatchQueue.main |
主线程(串行) | UI 更新 |
DispatchQueue.global() |
全局并发队列 | 网络请求、数据处理 |
OperationQueue |
基于 NSOperationQueue | 复杂依赖的任务管理 |
RunLoop.main |
主运行循环 | 兼容旧版 Timer 模式 |
ImmediateScheduler |
立即在当前线程执行 | 测试用 |
最佳实践 :始终在 subscribe(on:) 中指定后台队列执行耗时操作,在 receive(on:) 中切换到主线程更新 UI。遵循这一铁律,能避免绝大多数的线程问题。
7. 组合多个发布者
Combine 真正的威力在于组合操作符。多个发布者可以协同工作,构成复杂的异步逻辑管道。
| 操作符 | 行为 | 适用场景 |
|---|---|---|
combineLatest |
任一发布者产生新值,即发送组合元组 | 表单联合验证 |
merge(with:) |
交织合并多个流的值 | 多渠道消息聚合 |
zip |
严格一一配对两个流的值 | 同步并行请求 |
flatMap |
将上游值转为新的发布者并展平 | 搜索自动完成 |
switchToLatest |
仅保留最新的内部发布者的值 | 实时搜索(丢弃旧结果) |
prepend / append |
在流前后插入额外元素 | 添加默认值 |
7.1 combineLatest
combineLatest 订阅多个发布者,每当任何一个发布者发出新值时,与另一个发布者的最新值组合后一起发出:
swift
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<String, Never>()
pub1
.combineLatest(pub2)
.sink { intValue, stringValue in
print("Int: \(intValue), String: \(stringValue)")
}
pub1.send(1)
pub2.send("Hello") // 输出: Int: 1, String: Hello
pub1.send(2) // 输出: Int: 2, String: Hello
pub2.send("World") // 输出: Int: 2, String: World
重要 :combineLatest 需要所有参与发布者都至少发送过一次值之后,才会开始向下游发送组合值。
7.2 merge(with:)
merge 将多个同类型发布者的值交织合并到一个序列中:
swift
let pub1 = [1, 2, 3].publisher
let pub2 = [4, 5, 6].publisher
let pub3 = [7, 8, 9].publisher
pub1.merge(with: pub2, pub3)
.sink { value in
print(value) // 1-9 按发送顺序输出
}
7.3 zip
zip 将两个发布者的值按顺序严格配对------必须双方都有新值时才会发送组合元组:
swift
let numbers = [1, 2, 3, 4].publisher
let letters = ["A", "B", "C"].publisher // 注意:只有 3 个元素
numbers.zip(letters)
.sink { num, letter in
print("\(num): \(letter)")
}
// 输出:
// 1: A
// 2: B
// 3: C
// 注:4 没有被配对,因为 letters 已完成
7.4 flatMap
flatMap 将上游每个值转换为一个新的发布者 ,并将所有内部发布者的值"展平"到一个单一流中。与 switchToLatest 不同,它不会丢弃旧的内部发布者:
swift
let searchSubject = PassthroughSubject<String, Never>()
searchSubject
.flatMap { query in
// 每个搜索词创建一个网络请求 Publisher
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [SearchResult].self, decoder: JSONDecoder())
.catch { _ in Just([]) }
}
.sink { results in
print("搜索结果: \(results)")
}
searchSubject.send("swift")
searchSubject.send("combine")
7.5 CombineLatest3 / CombineLatest4
当需要联合验证多个字段时,Combine 提供了 CombineLatest3 和 CombineLatest4:
swift
Publishers.CombineLatest3($isUsernameValid, $isEmailValid, $isPasswordValid)
.map { $0 && $1 && $2 }
.assign(to: \.isFormValid, on: self)
.store(in: &cancellables)
注意 :CombineLatest3 最多支持四个发布者。如果超过四个,可以嵌套使用 combineLatest,或考虑使用 MergeMany 结合 collect 等其他策略。
8. Combine 与 SwiftUI 集成实战
Combine 与 SwiftUI 是天生的搭档。SwiftUI 的 @State 和 @Binding 属性包装器可以与 Combine 发布者无缝集成,实现视图与数据模型之间的响应式数据流。
8.1 经典架构:ObservableObject + @Published
swift
import SwiftUI
import Combine
class LoginViewModel: ObservableObject {
// 输入
@Published var username = ""
@Published var password = ""
// 输出
@Published var isLoginEnabled = false
@Published var isLoading = false
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
init() {
// 联合验证:两个字段都非空才启用按钮
Publishers.CombineLatest($username, $password)
.map { username, password in
!username.isEmpty && password.count >= 6
}
.assign(to: \.isLoginEnabled, on: self)
.store(in: &cancellables)
}
func login() {
isLoading = true
errorMessage = nil
// 模拟网络请求
Just((username, password))
.setFailureType(to: Error.self)
.flatMap { user, pass in
self.performLogin(username: user, password: pass)
}
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] _ in
self?.isLoading = false
print("登录成功")
}
)
.store(in: &cancellables)
}
private func performLogin(username: String, password: String) -> AnyPublisher<Void, Error> {
return Future { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
promise(.success(()))
}
}.eraseToAnyPublisher()
}
}
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
var body: some View {
VStack(spacing: 20) {
TextField("用户名", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
SecureField("密码(至少6位)", text: $viewModel.password)
.textFieldStyle(.roundedBorder)
if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.font(.caption)
}
Button(action: viewModel.login) {
if viewModel.isLoading {
ProgressView()
} else {
Text("登录")
}
}
.disabled(!viewModel.isLoginEnabled)
.buttonStyle(.borderedProminent)
}
.padding()
}
}
8.2 网络请求:URLSession 的 Combine 扩展
Combine 为 URLSession 提供了 dataTaskPublisher(for:) 方法,让网络请求与 Combine 管道无缝融合:
swift
struct User: Codable {
let id: Int
let name: String
let email: String
}
class APIService {
static let shared = APIService()
func fetchUser(id: Int) -> AnyPublisher<User, Error> {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
return Fail(error: URLError(.badURL))
.eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data) // 提取 Data
.decode(type: User.self, decoder: JSONDecoder()) // 解码为 User
.receive(on: DispatchQueue.main) // 切回主线程
.eraseToAnyPublisher() // 类型擦除
}
}
关于 eraseToAnyPublisher():
- 作用 :将具体发布者类型包装为
AnyPublisher<Output, Failure>,隐藏实现细节 - 优势 :简化函数返回类型,增强 API 灵活性;同时移除了
send()方法能力,事件只能通过订阅接收 - 使用时机:作为函数返回值时几乎必须使用,避免将复杂的具体类型暴露给调用方
8.3 搜索防抖 Debounce
搜索框是最适合 Combine 的场景之一------实时输入、防抖控制请求频率、取消旧请求:
swift
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [String] = []
@Published var isSearching = false
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) // 300ms 防抖
.removeDuplicates() // 去重
.filter { !$0.isEmpty }
.flatMap { [weak self] query -> AnyPublisher<[String], Never> in
self?.isSearching = true
return self?.performSearch(query) ?? Just([]).eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.sink { [weak self] results in
self?.results = results
self?.isSearching = false
}
.store(in: &cancellables)
}
private func performSearch(_ query: String) -> AnyPublisher<[String], Never> {
// 模拟搜索
return Just(["\(query) 结果 1", "\(query) 结果 2"])
.delay(for: .seconds(1), scheduler: DispatchQueue.global())
.eraseToAnyPublisher()
}
}
常用前置操作符速览:
debounce(for:scheduler:)--- 静默指定时间后才发送最新值throttle(for:scheduler:latest:)--- 限制发送频率removeDuplicates()--- 过滤连续相同的值(要求 Equatable)filter(_:)--- 条件过滤
9. Combine 与 Swift Concurrency 的共存
Combine 与 Swift 现代并发模型(async/await)并非互斥关系,而是互补关系。Combine 擅长处理连续的、高频的、可操作的事件流(如 UI 交互、传感器数据);async/await 擅长处理一次性的、线性的异步任务(如单个网络请求、文件 IO)。
9.1 将 Combine 流转换为 AsyncSequence
使用 .values 属性将 Publisher 转换为 AsyncSequence。这个机制允许你在 Task 中使用 for await 循环消费 Combine 流:
swift
let publisher = NotificationCenter.default
.publisher(for: .userDidLogin)
Task {
// 使用异步语法消费 Combine 流
for await notification in publisher.values {
print("用户已登录: \(notification)")
}
}
9.2 将 async 函数封装为 Publisher
使用 Future 将异步函数封装为 Combine 发布者:
swift
func fetchDataPublisher() -> AnyPublisher<Data, Error> {
Future { promise in
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
promise(.success(data))
} catch {
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
9.3 架构分工建议
在实际项目中,应遵循以下分层原则避免逻辑混乱:
| 维度 | 推荐 Combine | 推荐 Async/Await |
|---|---|---|
| 数据特性 | 多值流(持续更新) | 单值/元组(请求→响应) |
| 逻辑复杂度 | 需要高阶组合(zip/combineLatest) | 简单线性逻辑、循环、条件分支 |
| 生命周期 | 需要精确的 cancel 控制 | 依赖 Task 作用域自动取消 |
| UI 绑定 | @Published 与 SwiftUI 深度绑定 | 配合 @MainActor 处理简单更新 |
9.4 转换实战:AnyPublisher 到 async throws
将 AnyPublisher<Void, Error> 转换为 async throws 方法:
swift
protocol SomeProtocol {
func performAction() async throws
}
class SomeImplementation: SomeProtocol {
func something() -> AnyPublisher<Void, Error> {
// ... 现有的 Combine 实现
}
func performAction() async throws {
let publishedValues = something().values
var iterator = publishedValues.makeAsyncIterator()
try await iterator.next() // 等待第一个值或完成
print("操作完成")
}
}
9.5 Combine 到 AsyncAlgorithms 的迁移
2022 年 Apple 开源了 Swift Async Algorithms 包,它提供了许多与 Combine 操作符对应的异步序列版本(如 merge、zip、debounce 等),使得开发者可以将大部分 Combine 功能迁移到 Swift 原生并发模型上。
逐步迁移策略:
- 识别代码中的异步边界
- 从简单的 Combine 链开始逐步替换为 async/await
- 使用
.values将 Publisher 转换为 AsyncSequence - 对每一步迁移进行充分测试
10. 调试技巧
10.1 print() 操作符
print() 是 Combine 最便捷的调试工具,它会在每个事件发生时输出日志:
swift
publisher
.print("🔍 调试标签") // 为日志添加前缀
.sink { value in
print(value)
}
// 输出示例:
// 🔍 调试标签: receive subscription: (PassthroughSubject)
// 🔍 调试标签: request unlimited
// 🔍 调试标签: receive value: (你好)
// 🔍 调试标签: receive finished
10.2 handleEvents 操作符
handleEvents 可以观察数据流中的所有事件而不影响数据本身,是定位问题的利器:
swift
publisher
.handleEvents(
receiveSubscription: { subscription in
print("✅ 收到订阅: \(subscription)")
},
receiveOutput: { value in
print("📤 收到值: \(value)")
},
receiveCompletion: { completion in
print("🏁 完成: \(completion)")
},
receiveCancel: {
print("❌ 订阅被取消")
},
receiveRequest: { demand in
print("📥 请求数量: \(demand)")
}
)
.sink { value in
print("最终处理: \(value)")
}
10.3 breakpoint / breakpointOnError
在调试时暂停执行,检查调用栈:
swift
publisher
.breakpoint(
receiveSubscription: { _ in false },
receiveOutput: { value in
// 当值为负数时暂停调试器
return (value as? Int ?? 0) < 0
},
receiveCompletion: { _ in false }
)
.sink { value in ... }
// 简化版:仅错误时暂停
publisher
.breakpointOnError()
.sink { value in ... }
10.4 常见问题排查清单
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 订阅不触发 | 未存储 AnyCancellable | 使用 .store(in:) |
| 重复触发 | 多次订阅 | 检查是否在 body 中订阅 |
| 内存泄漏 | 循环引用 | 使用 [weak self] |
| 值不更新 | 线程问题 | 添加 receive(on:) |
| 操作符无效 | 类型不匹配 | 使用 print() 排查 |
11. @Observable 与 Combine:iOS 17 的范式转变
11.1 iOS 17+ 的新观察模式
iOS 17 引入的 @Observable 宏改变了 SwiftUI 的观察模式。Apple 实际上已经有效地弃用了 ObservableObject 协议和 @Published 属性包装器的旧范式,但其替代方案 @Observable 宏和 withObservationTracking 函数目前只提供了部分功能。
从旧范式的 @Published + ObservableObject 迁移到新的 @Observable 宏时,开发者面临的一个常见挑战是:Combine 自动创建的 Publisher(如 $propertyName)不再可用。
11.2 手动桥接 Combine Publisher
在 @Observable 类中使用 CurrentValueSubject 手动创建 Publisher:
swift
import SwiftUI
import Combine
import Observation
@Observable
class UserSettings {
var isNotificationsEnabled = false {
didSet {
isNotificationsEnabled$.send(isNotificationsEnabled)
}
}
// 手动创建对应的 Combine Publisher
var isNotificationsEnabled$ = CurrentValueSubject<Bool, Never>(false)
}
// 在需要 Combine 管道的地方
let settings = UserSettings()
settings.isNotificationsEnabled$
.sink { newValue in
print("通知设置变更: \(newValue)")
}
.store(in: &cancellables)
这种方案通过在 didSet 中调用 send() 来保持 Combine 兼容性,适合从旧项目逐步迁移的场景。
11.3 新旧方式对比
| 特性 | ObservableObject + @Published | @Observable |
|---|---|---|
| 自动 Publisher | ✅ $property 自动生成 |
❌ 需手动创建 CurrentValueSubject |
| 视图更新粒度 | 整个对象 | 仅被使用的属性 |
| 性能 | 较低(全量刷新) | 较高(按需刷新) |
| 最低系统要求 | iOS 13+ | iOS 17+ |
| Combine 集成 | 原生支持 | 需手动桥接 |
12. Combine 最佳实践
12.1 架构原则
- 依赖抽象而非具体实现 :对外暴露
AnyPublisher<Output, Failure>而非具体类型 - 集中管理订阅 :统一使用
Set<AnyCancellable>和.store(in:) - 隔离副作用:将网络请求、数据库操作等封装在专用 Service 层
- 避免在 View 中直接订阅 :View 只负责渲染,不要在其中使用
.sink;应在 ViewModel 中订阅
12.2 性能考量
- 避免过度使用操作符:每个操作符都会引入额外的内存分配和闭包调用
- 使用
share()避免重复订阅:当多个订阅者需要共享同一个上游响应时 - 注意线程切换开销 :不必要的
receive(on:)会增加调度成本 - 使用
eraseToAnyPublisher()来隐藏复杂类型,同时在必要时启用类型信息传递
12.3 内存管理
swift
class MyViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
func setupSubscriptions() {
// ✅ 正确:使用 .store(in:)
somePublisher
.sink { [weak self] value in
self?.handleValue(value)
}
.store(in: &cancellables)
}
// ❌ 错误:忘记 .store(in:) 会导致订阅立即取消
func badSetup() {
somePublisher
.sink { value in
print(value)
}
// AnyCancellable 未被持有,订阅立即取消!
}
}
13. 总结
本章全面介绍了 Combine 框架的核心概念与实践技法:
- 核心三角:Publisher(发布者)产生数据,Operator(操作符)转换数据,Subscriber(订阅者)消费数据
- 内置发布者:Just、Future、Subject、Timer 等覆盖了大多数使用场景
- 内存管理 :
Set<AnyCancellable>+.store(in:)是标准模式,[weak self]防止循环引用 - 错误处理:tryMap、catch、retry、replaceError 构成完整的错误恢复链路
- 线程控制 :
subscribe(on:)指定执行队列,receive(on:)指定回调队列 - 组合能力:combineLatest、zip、merge、flatMap 让异步逻辑表达力强大
- SwiftUI 集成:@Published + ObservableObject 构建响应式 ViewModel
- 现代并发共存 :
.values桥接 Combine 与 async/await - iOS 17+ 迁移 :@Observable 下用
CurrentValueSubject保持 Combine 兼容性
Combine 的学习曲线虽然陡峭,但掌握之后会发现它是一把打开 Swift 响应式编程世界的钥匙。建议从简单的 Just 和 sink 开始,逐步探索更复杂的操作符组合,最终建立起"一切皆流"的编程思维。
只有亲自动手敲代码、踩坑、调试,才能真正领悟 Combine 的精妙之处。从今天开始,把第一个 publisher 变成你项目的一部分吧。
14. 参考资源
- Apple Developer - Combine Framework
- Apple Developer - Using Combine for Your App's Asynchronous Code
- Swift Async Algorithms - GitHub
- WWDC23 - Discover Observation in SwiftUI
- Swift with Majid - Discovering Swift Async Algorithms package
- Hacking with Swift - How to migrate @Published with Combine to the new @Observable