📋 目录
一、Combine框架使用详解
1. Combine框架概述
Combine 是 Apple 在 WWDC 2019 推出的响应式编程框架,用于处理异步事件流。它基于 ReactiveX 的设计思想,提供了声明式的 API 来处理时间序列数据。
1.1 什么是Combine
Combine 是一个声明式的 Swift 框架,用于处理随时间变化的值。它允许你通过组合(combine)不同的操作符来创建复杂的数据处理管道。
核心特点:
- 声明式编程:描述"做什么"而不是"怎么做"
- 函数式编程:使用高阶函数和操作符组合
- 类型安全:充分利用 Swift 的类型系统
- 异步处理:优雅地处理异步操作
- 错误处理:统一的错误处理机制
1.2 Combine vs 其他框架
| 特性 | Combine | RxSwift | ReactiveSwift |
|---|---|---|---|
| 平台 | Apple 生态(iOS 13+) | 跨平台 | 跨平台 |
| 语言 | Swift | Swift | Swift |
| 官方支持 | ✅ Apple 官方 | ❌ 第三方 | ❌ 第三方 |
| 性能 | 高度优化 | 良好 | 良好 |
| 学习曲线 | 中等 | 陡峭 | 陡峭 |
| 与系统集成 | 深度集成 | 需要适配 | 需要适配 |
1.3 适用场景
- 网络请求:处理 API 响应
- 用户输入:处理文本输入、按钮点击
- 数据绑定:UI 与数据模型的双向绑定
- 状态管理:管理应用状态变化
- 事件处理:处理通知、定时器等事件
1.4 编程思想(背后的范式与理念)
Combine 的 API 和设计深受几种编程思想影响,理解这些思想能更快抓住「为什么这样写」而不是「怎么背 API」。
(1)响应式编程(Reactive Programming)
- 核心:把「数据与事件」抽象成随时间推进的流,通过订阅对流中的每个值做出反应,而不是轮询或回调嵌套。
- 在 Combine 中:
Publisher就是一条流,Subscriber订阅后对每个receive(_ input:)做出反应;用户输入、网络结果、定时器都可以统一成同一种「流」,用同一套操作符处理。 - 与命令式的对比:命令式是「先做 A,再做 B,再根据结果做 C」;响应式是「当流里出现满足某条件的数据时,做 C」,逻辑由数据驱动。
(2)声明式 vs 命令式
| 维度 | 命令式(Imperative) | 声明式(Declarative) |
|---|---|---|
| 关注点 | 「怎么做」:一步步写清执行顺序与分支 | 「做什么」:描述期望的结果与约束 |
| 典型写法 | 循环、if-else、回调里再调回调 | 链式操作符:map / filter / combineLatest |
| 在 Combine 中 | 手写「请求 → 等回调 → 解析 → 再请求」 | 用 publisher.map(...).flatMap(...).sink(...) 描述数据如何变换与消费 |
声明式让「数据流」一目了然,可读性和可测试性更好;Combine 的链式调用就是声明式的一种体现。
(3)函数式思想(Composition & Immutability)
- 组合(Composition):小能力组合成大能力。每个操作符只做一件事(map 只做变换、filter 只做过滤),通过
.map(...).filter(...)组合成完整管道,而不是写一个巨大的闭包。 - 不可变(Immutability):操作符不修改上游 Publisher,而是返回新的 Publisher;上游保持不变,便于推理和复用。
- 纯函数倾向:变换用无副作用的闭包(给定相同输入得到相同输出),副作用集中在
sink或assign等「终端」处,便于测试和并发。
(4)流与时间序列(Streams & Time)
- 把一切可观测的「变化」都看成时间上的序列:第 1 个值、第 2 个值、……、完成或错误。
- 操作符可以针对「时间」语义:
debounce(等一段时间再发)、throttle(间隔内只发一次)、delay(延后发射),从而统一处理「何时」而不只是「何值」。
(5)观察者与发布-订阅(Observer & Pub-Sub)
- 观察者模式:观察者订阅被观察对象,在状态变化时得到通知。Combine 里 Subscriber 观察 Publisher。
- 发布-订阅:发布者与订阅者解耦,通过「订阅」建立连接;Combine 用
Subscription表示这次连接,用request(Demand)控制拉取节奏,是带背压的发布-订阅。
把以上几点串起来:Combine 用声明式的流(Publisher)和组合式的操作符,在发布-订阅模型下做响应式的数据与事件处理,并借 Scheduler 控制时间与线程。理解这些思想后,再看到「为什么用 map 而不是在 sink 里写一坨」「为什么要 subscribe(on:) / receive(on:)」就会更自然。
1.5 原理概览(为何这样设计)
Combine 的核心理念可以概括为以下几点,便于后续理解「架构」与「信息流」:
| 理念 | 说明 |
|---|---|
| 发布-订阅 | Publisher 不主动推数据,只有 Subscriber 通过 Subscription.request(demand) 请求后,才按需发送;这样下游可以控制节奏,避免被上游淹没。 |
| 背压(Backpressure) | Subscriber.receive(_ input:) 的返回值类型是 Subscribers.Demand,表示「还能再要多少」;上游根据 Demand 决定是否继续发送,实现流控。 |
| 链式不可变 | 每个操作符(map、filter 等)都返回新的 Publisher,不修改原 Publisher;整条链是值类型组合,易于推理和测试。 |
| 调度与线程 | 谁在哪个线程执行由 Scheduler 决定;subscribe(on:) 指定「上游与订阅建立」所在线程,receive(on:) 指定「下游收值」所在线程,便于 UI 与后台分离。 |
后续「二、源码解析」中的内部架构、响应者链、信息流会与上述四点一一对应。
2. 核心概念
2.1 Publisher(发布者)
Publisher 是 Combine 的核心协议,表示可以发布值的类型。
protocol Publisher {
associatedtype Output
associatedtype Failure: Error
func receive<S>(subscriber: S) where S: Subscriber,
S.Input == Output, S.Failure == Failure
}
特点:
- 可以发布零个或多个值
- 可能以完成或错误结束
- 是值类型(struct)
- 不可变(每次操作返回新的 Publisher)
示例:
// 创建一个简单的 Publisher:Just 发布单个值后立即完成
let publisher = Just("Hello, Combine!")
.sink { value in
print(value) // 输出: Hello, Combine!
}
// 使用 Sequence 的 publisher 扩展,将数组转为发布者,按序发布每个元素
let arrayPublisher = [1, 2, 3, 4, 5].publisher
.sink { value in
print(value) // 依次输出: 1, 2, 3, 4, 5
}
2.2 Subscriber(订阅者)
Subscriber 是接收 Publisher 发布值的协议。
protocol Subscriber: CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure: Error
func receive(subscription: Subscription)
func receive(_ input: Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Failure>)
}
内置 Subscriber:
sink:最简单的订阅方式assign:将值赋给对象的属性
示例:
// 使用 sink 订阅:同时处理「值」与「完成/错误」
let cancellable = [1, 2, 3].publisher
.sink(
receiveCompletion: { completion in
// 流结束时的回调:.finished 或 .failure(error)
switch completion {
case .finished:
print("完成")
case .failure(let error):
print("错误: \(error)")
}
},
receiveValue: { value in
print("收到值: \(value)")
}
)
// 使用 assign 订阅:将每个发布的值赋给对象的某个属性(KeyPath)
class ViewModel {
@Published var count: Int = 0
}
let viewModel = ViewModel()
let cancellable = [1, 2, 3].publisher
.assign(to: \.count, on: viewModel) // 最终 viewModel.count == 3
2.3 Subscription(订阅)
Subscription 表示订阅关系,控制数据流的生命周期。
protocol Subscription: Cancellable, CustomCombineIdentifierConvertible {
func request(_ demand: Subscribers.Demand)
}
关键点:
- 控制数据流的开始和结束
- 实现背压(backpressure)控制
- 可以取消订阅
示例:
// 自定义 Subscriber,演示背压:通过 request(.max(3)) 只拉取 3 个值
class CustomSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
// 建立订阅后,主动请求最多 3 个值(背压控制)
subscription.request(.max(3))
}
func receive(_ input: Int) -> Subscribers.Demand {
print("收到: \(input)")
// 返回 .none 表示本轮不再请求更多;上游最多只会发 3 个
return .none
}
func receive(completion: Subscribers.Completion<Never>) {
print("完成")
}
}
let subscriber = CustomSubscriber()
// 数组有 5 个元素,但只会收到 1、2、3
[1, 2, 3, 4, 5].publisher.subscribe(subscriber)
3. Publisher与Subscriber
3.1 内置Publisher类型
Just
发布单个值然后完成。
// Just:有订阅时发布一个值并立即发送 .finished
let just = Just("Hello")
.sink { value in
print(value) // 输出: Hello
}
Future
异步执行操作并发布结果。
// Future:封装异步回调,只执行一次,结果通过 promise 发布
func fetchData() -> Future<String, Error> {
return Future { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
promise(.success("数据加载完成"))
}
}
}
fetchData()
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("错误: \(error)")
}
},
receiveValue: { value in
print(value) // 输出: 数据加载完成
}
)
Deferred
延迟创建 Publisher,直到有订阅者。
// Deferred:闭包在「第一次被订阅」时才执行,避免创建时就产生副作用
let deferred = Deferred {
Future { promise in
print("开始执行")
promise(.success("结果"))
}
}
// 此时不会执行(只创建了 Deferred,未订阅)
print("创建完成")
// 订阅时才执行内部 Future,并收到 "结果"
deferred.sink { value in
print(value) // 输出: 开始执行, 结果
}
Empty
不发布任何值,可选择立即完成或永不完成。Empty 是 Combine 中非常有用的占位符 Publisher,常用于条件分支、错误处理、以及保持订阅活跃。
基本用法:
// 立即完成:不发送任何 value,只发送 completion
let empty = Empty<String, Never>(completeImmediately: true)
.sink(
receiveCompletion: { _ in print("完成") },
receiveValue: { _ in }
)
// 永不完成:既不发值也不发 completion,常用于测试或「占位」
let never = Empty<String, Never>(completeImmediately: false)
Empty 的占位操作:
Empty 最常见的用途是作为占位符 Publisher,在条件不满足时提供一个"空"的 Publisher,避免返回 Optional 或处理 nil 的情况。
1. 条件分支中的占位
// 场景:根据条件返回不同的 Publisher
func fetchData(shouldFetch: Bool) -> AnyPublisher<String, Never> {
if shouldFetch {
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.compactMap { String(data: $0, encoding: .utf8) }
.replaceError(with: "")
.eraseToAnyPublisher()
} else {
// 使用 Empty 作为占位,不执行任何操作
return Empty(completeImmediately: true)
.eraseToAnyPublisher()
}
}
// 使用:无论条件如何,返回类型都是 AnyPublisher<String, Never>
fetchData(shouldFetch: true)
.sink { print($0) } // 正常接收数据
fetchData(shouldFetch: false)
.sink { print($0) } // 立即完成,不接收任何值
2. 错误处理中的占位
// 在 catch 中使用 Empty 作为备用 Publisher
func loadUserData() -> AnyPublisher<User, Error> {
return URLSession.shared.dataTaskPublisher(for: userURL)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.catch { error -> AnyPublisher<User, Error> in
if error is DecodingError {
// 解码错误时返回空 Publisher,不发送任何值
return Empty(completeImmediately: true)
.eraseToAnyPublisher()
} else {
// 其他错误继续传播
return Fail(error: error)
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
3. flatMap 中的条件占位
// 在 flatMap 中根据条件决定是否执行操作
class SearchViewModel: ObservableObject {
@Published var searchText: String = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.flatMap { query -> AnyPublisher<[String], Never> in
if query.isEmpty {
// 空查询时返回 Empty,不执行搜索
return Empty(completeImmediately: true)
.eraseToAnyPublisher()
} else {
// 执行搜索
return self.search(query: query)
.catch { _ in Just([]) }
.eraseToAnyPublisher()
}
}
.assign(to: \.results, on: self)
.store(in: &cancellables)
}
private func search(query: String) -> AnyPublisher<[String], Error> {
// 搜索实现
return Just(["结果1", "结果2"])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
4. 使用 Empty 保持订阅活跃(常驻任务)
Empty 的 completeImmediately: false 模式可以创建一个永不完成的 Publisher,这在需要保持订阅活跃、贯穿整个程序生命周期的场景中非常有用。
场景1:常驻的后台任务
class BackgroundTaskManager {
private var cancellables = Set<AnyCancellable>()
// 创建一个永不完成的 Empty 作为基础流
private let keepAlive = Empty<Never, Never>(completeImmediately: false)
.eraseToAnyPublisher()
func startBackgroundTask() {
// 使用 flatMap 将 Empty 转换为周期性的任务流
keepAlive
.flatMap { _ -> AnyPublisher<Date, Never> in
// 每 5 秒执行一次任务
return Timer.publish(every: 5.0, on: .main, in: .common)
.autoconnect()
.map { _ in Date() }
.eraseToAnyPublisher()
}
.sink { [weak self] date in
self?.performBackgroundTask(at: date)
}
.store(in: &cancellables)
}
private func performBackgroundTask(at date: Date) {
print("执行后台任务: \(date)")
// 执行实际的后台任务,如数据同步、状态检查等
}
func stopBackgroundTask() {
cancellables.removeAll() // 取消所有订阅
}
}
场景2:常驻的事件监听
class AppLifecycleManager {
private var cancellables = Set<AnyCancellable>()
func startMonitoring() {
// 使用 Empty 作为基础流,保持订阅活跃
Empty<Never, Never>(completeImmediately: false)
.flatMap { _ -> AnyPublisher<Notification, Never> in
// 监听多个通知
let appWillEnterForeground = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
let appDidEnterBackground = NotificationCenter.default
.publisher(for: UIApplication.didEnterBackgroundNotification)
// 合并多个通知流
return Publishers.Merge(appWillEnterForeground, appDidEnterBackground)
.eraseToAnyPublisher()
}
.sink { [weak self] notification in
self?.handleAppLifecycleEvent(notification)
}
.store(in: &cancellables)
}
private func handleAppLifecycleEvent(_ notification: Notification) {
switch notification.name {
case UIApplication.willEnterForegroundNotification:
print("应用即将进入前台")
case UIApplication.didEnterBackgroundNotification:
print("应用进入后台")
default:
break
}
}
}
场景3:常驻的心跳/保活机制
class HeartbeatManager {
private var cancellables = Set<AnyCancellable>()
private let heartbeatInterval: TimeInterval = 30.0
func startHeartbeat() {
// 使用 Empty 保持订阅,然后转换为心跳流
Empty<Never, Never>(completeImmediately: false)
.flatMap { [weak self] _ -> AnyPublisher<Void, Never> in
guard let self = self else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
// 创建心跳定时器
return Timer.publish(every: self.heartbeatInterval, on: .main, in: .common)
.autoconnect()
.map { _ in () }
.eraseToAnyPublisher()
}
.sink { [weak self] _ in
self?.sendHeartbeat()
}
.store(in: &cancellables)
}
private func sendHeartbeat() {
// 发送心跳请求
print("发送心跳: \(Date())")
// 实际实现:发送网络请求保持连接活跃
}
func stopHeartbeat() {
cancellables.removeAll()
}
}
场景4:常驻的数据同步任务
class DataSyncManager {
private var cancellables = Set<AnyCancellable>()
private let syncInterval: TimeInterval = 60.0
func startAutoSync() {
// 使用 Empty 作为基础流,保持订阅贯穿应用生命周期
Empty<Never, Never>(completeImmediately: false)
.flatMap { [weak self] _ -> AnyPublisher<SyncResult, Never> in
guard let self = self else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
// 创建周期性同步流
return Timer.publish(every: self.syncInterval, on: .main, in: .common)
.autoconnect()
.flatMap { _ -> AnyPublisher<SyncResult, Never> in
return self.performSync()
.catch { _ in Just(SyncResult.failure) }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.sink { [weak self] result in
self?.handleSyncResult(result)
}
.store(in: &cancellables)
}
private func performSync() -> AnyPublisher<SyncResult, Error> {
// 执行数据同步
return Just(SyncResult.success)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
private func handleSyncResult(_ result: SyncResult) {
print("同步结果: \(result)")
}
enum SyncResult {
case success
case failure
}
}
场景5:更优雅的常驻任务实现(推荐方式)
虽然 Empty 可以用来保持订阅,但更推荐使用 Timer.publish().autoconnect() 或 PassthroughSubject 来实现常驻任务:
class BetterBackgroundTaskManager {
private var cancellables = Set<AnyCancellable>()
func startBackgroundTask() {
// 方式1:直接使用 Timer(推荐)
Timer.publish(every: 5.0, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.performTask()
}
.store(in: &cancellables)
// 方式2:使用 PassthroughSubject 控制(更灵活)
let taskTrigger = PassthroughSubject<Void, Never>()
taskTrigger
.sink { [weak self] _ in
self?.performTask()
}
.store(in: &cancellables)
// 可以手动触发或结合 Timer
Timer.publish(every: 5.0, on: .main, in: .common)
.autoconnect()
.sink { _ in taskTrigger.send() }
.store(in: &cancellables)
}
private func performTask() {
print("执行任务")
}
}
Empty 占位操作的最佳实践:
- 类型一致性:使用 Empty 时确保类型匹配(Output 和 Failure)
- 立即完成 vs 永不完成:
completeImmediately: true:用于条件分支,表示"跳过此分支"completeImmediately: false:用于保持订阅活跃,但更推荐使用 Timer 或 Subject
- 结合 eraseToAnyPublisher():在使用 Empty 时通常需要类型擦除,以保持类型一致性
- 避免过度使用:对于常驻任务,优先考虑 Timer 或 PassthroughSubject,Empty 更适合作为占位符
Empty 的常见使用模式总结:
| 使用场景 | completeImmediately | 说明 |
|---|---|---|
| 条件分支占位 | true | 条件不满足时返回空流 |
| 错误处理占位 | true | 某些错误情况下不发送值 |
| 测试占位 | false | 测试中模拟永不完成的流 |
| 保持订阅(不推荐) | false | 可用但更推荐 Timer/Subject |
注意事项:
- Empty 是值类型(struct),每次创建都是新实例
completeImmediately: false的 Empty 会保持订阅活跃,但不会发送任何值- 对于常驻任务,虽然可以用 Empty 实现,但使用 Timer 或 PassthroughSubject 更直观和高效
Fail
立即发布错误。
// Fail:有订阅时立即发送 .failure(error),不发送任何正常值
enum MyError: Error {
case customError
}
let fail = Fail<String, MyError>(error: .customError)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("错误: \(error)")
}
},
receiveValue: { _ in }
)
Sequence
从序列创建 Publisher。
// 符合 Sequence 的类型都有 .publisher,按顺序发布元素
let sequence = (1...5).publisher
.sink { value in
print(value) // 输出: 1, 2, 3, 4, 5
}
3.2 自定义Publisher
// 自定义 Publisher:从数组按需发布元素,遵循背压
struct CustomPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
let values: [Int]
func receive<S>(subscriber: S) where S: Subscriber,
S.Input == Output, S.Failure == Failure {
// 收到订阅者时,创建自定义 Subscription 并下发给订阅者
let subscription = CustomSubscription(
subscriber: subscriber,
values: values
)
subscriber.receive(subscription: subscription)
}
}
// 自定义 Subscription:根据 request(demand) 按需从 values 取数并下发
class CustomSubscription<S: Subscriber>: Subscription
where S.Input == Int, S.Failure == Never {
var subscriber: S?
let values: [Int]
var currentIndex = 0
var requested: Subscribers.Demand = .none
init(subscriber: S, values: [Int]) {
self.subscriber = subscriber
self.values = values
}
func request(_ demand: Subscribers.Demand) {
requested += demand
// 在 demand 允许且还有数据时,逐个下发
while requested > .none && currentIndex < values.count {
let value = values[currentIndex]
currentIndex += 1
requested -= .max(1)
_ = subscriber?.receive(value)
}
if currentIndex >= values.count {
subscriber?.receive(completion: .finished)
cancel()
}
}
func cancel() {
subscriber = nil
}
}
// 使用自定义 Publisher:行为等价于 [1,2,3].publisher
let custom = CustomPublisher(values: [1, 2, 3])
.sink { value in
print(value) // 输出: 1, 2, 3
}
4. Operators操作符
4.1 转换操作符
map
转换每个值。
// map:对每个元素做变换,类型可改变
[1, 2, 3].publisher
.map { $0 * 2 }
.sink { print($0) } // 输出: 2, 4, 6
flatMap
将多个 Publisher 扁平化。
// flatMap:每个元素映射为一个新 Publisher,再把这些 Publisher 的输出「压平」成一条流
["A", "B", "C"].publisher
.flatMap { letter in
(1...2).publisher.map { "\(letter)\($0)" }
}
.sink { print($0) } // 输出: A1, A2, B1, B2, C1, C2
compactMap
过滤 nil 值。
// compactMap:类似 map,但闭包返回 Optional;nil 会被丢弃,不往下游发
["1", "2", "abc", "3"].publisher
.compactMap { Int($0) }
.sink { print($0) } // 输出: 1, 2, 3
scan
累积值。
// scan:给定初始值,每收到一个元素就与当前累积值做运算,并下发新的累积值
[1, 2, 3, 4, 5].publisher
.scan(0, +)
.sink { print($0) } // 输出: 1, 3, 6, 10, 15
4.2 过滤操作符
filter
过滤值。
// filter:只下发谓词为 true 的值
[1, 2, 3, 4, 5].publisher
.filter { $0 % 2 == 0 }
.sink { print($0) } // 输出: 2, 4
removeDuplicates
移除重复值。
// removeDuplicates:连续相同只发第一个,相当于「相邻去重」
[1, 1, 2, 2, 3, 3].publisher
.removeDuplicates()
.sink { print($0) } // 输出: 1, 2, 3
first / last
获取第一个或最后一个值。
// first:只取第一个元素,取到后发完成
[1, 2, 3, 4, 5].publisher
.first()
.sink { print($0) } // 输出: 1
// last:必须等上游完成,再发最后一个元素
[1, 2, 3, 4, 5].publisher
.last()
.sink { print($0) } // 输出: 5
dropFirst / dropLast
丢弃前几个或后几个值。
// dropFirst(n):跳过前 n 个,只发后面的
[1, 2, 3, 4, 5].publisher
.dropFirst(2)
.sink { print($0) } // 输出: 3, 4, 5
4.3 组合操作符
combineLatest
组合多个 Publisher 的最新值。
// combineLatest:两边都至少发过一个值后,每次任一边发新值就组合「两边当前最新值」下发
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1
.combineLatest(publisher2)
.sink { value1, value2 in
print("\(value1): \(value2)")
}
publisher1.send("A") // 无输出(publisher2 尚未发过值)
publisher2.send(1) // 输出: A: 1
publisher1.send("B") // 输出: B: 1(用 B 与 2 的最新值 1 组合)
publisher2.send(2) // 输出: B: 2
merge
合并多个 Publisher。
// merge:多个流合并成一条,哪个先发就先收到哪个,类型必须相同
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1
.merge(with: publisher2)
.sink { print($0) }
publisher1.send(1) // 输出: 1
publisher2.send(2) // 输出: 2
publisher1.send(3) // 输出: 3
zip
按顺序组合多个 Publisher。
// zip:按「第 n 个与第 n 个」配对,凑齐一对才下发,顺序严格
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1
.zip(publisher2)
.sink { value1, value2 in
print("\(value1): \(value2)")
}
publisher1.send("A") // 等待 publisher2 的第一个值
publisher1.send("B") // 等待 publisher2 的第二个值
publisher2.send(1) // 输出: A: 1
publisher2.send(2) // 输出: B: 2
4.4 时间操作符
debounce
防抖,等待指定时间后发布最新值。
// debounce:在一段时间内没有新值时,才把「最后一次收到的值」发出去(适合搜索框)
let subject = PassthroughSubject<String, Never>()
subject
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink { print($0) }
subject.send("H") // 不输出(等待 0.5s)
subject.send("He") // 重置等待
subject.send("Hel") // 重置等待
subject.send("Hell") // 重置等待
subject.send("Hello") // 0.5 秒内无新值,输出: Hello
throttle
节流,在指定时间间隔内只发布第一个值。
// throttle:在时间窗口内只取一个值;latest: false 取窗口内第一个,true 取最后一个
let subject = PassthroughSubject<String, Never>()
subject
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: false)
.sink { print($0) }
subject.send("A") // 立即输出: A,开启 1 秒窗口
subject.send("B") // 不输出(1 秒内)
subject.send("C") // 不输出(1 秒内)
// 1 秒后
subject.send("D") // 输出: D
delay
延迟发布值。
// delay:每个元素都延后指定时间再下发,相对顺序不变
[1, 2, 3].publisher
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.sink { print($0) } // 1 秒后依次输出: 1, 2, 3
4.5 错误处理操作符
catch
捕获错误并返回备用 Publisher。
// catch:上游失败时用闭包返回一个备用 Publisher,流继续用备用流
enum MyError: Error {
case failure
}
let publisher = Fail<String, MyError>(error: .failure)
.catch { error -> Just<String> in
print("捕获错误: \(error)")
return Just("备用值")
}
.sink { print($0) } // 输出: 捕获错误: failure, 备用值
retry
重试失败的 Publisher。
// retry(n):失败时重新订阅上游最多 n 次(这里是 2 次,共最多 3 次执行)
var attempts = 0
let publisher = Future<String, Error> { promise in
attempts += 1
if attempts < 3 {
promise(.failure(NSError(domain: "test", code: 1)))
} else {
promise(.success("成功"))
}
}
.retry(2) // 最多重试 2 次,第 3 次成功
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) } // 输出: 成功
)
replaceError
用默认值替换错误。
// replaceError:失败时不发错误,改为发一个默认值并正常结束
let publisher = Fail<String, MyError>(error: .failure)
.replaceError(with: "默认值")
.sink { print($0) } // 输出: 默认值
5. Subjects
Subjects 既是 Publisher 又是 Subscriber,可以手动发送值。
5.1 PassthroughSubject
直接传递值,不保存当前值。
// PassthroughSubject:只转发 send 的值,不存当前值,后订阅的收不到之前的值
let subject = PassthroughSubject<String, Never>()
// 订阅1
let cancellable1 = subject.sink { print("订阅1: \($0)") }
subject.send("A") // 输出: 订阅1: A
// 订阅2:之后 send 的值两个订阅都会收到
let cancellable2 = subject.sink { print("订阅2: \($0)") }
subject.send("B") // 输出: 订阅1: B, 订阅2: B
5.2 CurrentValueSubject
保存当前值,新订阅者会立即收到当前值。
// CurrentValueSubject:持有当前 value,新订阅者会先收到当前值再收后续 send
let subject = CurrentValueSubject<String, Never>("初始值")
// 订阅1:立即收到初始值
let cancellable1 = subject.sink { print("订阅1: \($0)") }
// 输出: 订阅1: 初始值
subject.value = "新值" // 输出: 订阅1: 新值
// 订阅2:一订阅就收到当前值 "新值"
let cancellable2 = subject.sink { print("订阅2: \($0)") }
// 输出: 订阅2: 新值(立即收到当前值)
5.3 @Published 属性包装器
自动创建 Publisher。
// @Published:属性变化时自动发值;$name 是该属性的 Publisher
class ViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
init() {
// 监听 name 的变化,防抖后处理
$name
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink { [weak self] newName in
print("名称变化: \(newName)")
}
.store(in: &cancellables)
}
private var cancellables = Set<AnyCancellable>()
}
let viewModel = ViewModel()
viewModel.name = "张三" // 0.5 秒后输出: 名称变化: 张三
5.4 把属性变成 Publisher 的使用案例
在 Combine 中,有多种方式可以将属性转换为 Publisher,每种方式适用于不同的场景。理解这些方式有助于更好地使用 Combine 进行响应式编程。
5.4.1 使用 @Published 属性包装器(推荐)
@Published 是 Combine 中最常用和推荐的方式,特别适合在 ViewModel 或 ObservableObject 中使用。
基本用法:
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
@Published var isLoggedIn: Bool = false
private var cancellables = Set<AnyCancellable>()
init() {
// 使用 $name 访问 Publisher
$name
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.sink { [weak self] newName in
print("名称变化: \(newName)")
// 可以触发其他操作,如搜索、验证等
}
.store(in: &cancellables)
// 监听多个属性
Publishers.CombineLatest($name, $age)
.sink { [weak self] name, age in
print("用户信息: \(name), \(age)")
}
.store(in: &cancellables)
}
}
特点:
- ✅ 自动创建 Publisher(通过
$属性名访问) - ✅ 类型安全,编译时检查
- ✅ 与 SwiftUI 深度集成
- ✅ 自动发送初始值(可通过
dropFirst()跳过)
5.4.2 使用 CurrentValueSubject
CurrentValueSubject 适合需要手动控制发布时机的场景,或者需要将非 @Published 属性转换为 Publisher。
基本用法:
class SettingsManager {
// 方式1:直接使用 CurrentValueSubject 作为存储属性
private let _theme = CurrentValueSubject<String, Never>("light")
var theme: String {
get { _theme.value }
set { _theme.value = newValue }
}
// 暴露 Publisher
var themePublisher: AnyPublisher<String, Never> {
_theme.eraseToAnyPublisher()
}
// 方式2:将普通属性包装为 CurrentValueSubject
private var _userName: String = ""
private let userNameSubject = CurrentValueSubject<String, Never>("")
var userName: String {
get { _userName }
set {
_userName = newValue
userNameSubject.send(newValue)
}
}
var userNamePublisher: AnyPublisher<String, Never> {
userNameSubject.eraseToAnyPublisher()
}
}
实际应用场景:
class NetworkManager {
private let _connectionStatus = CurrentValueSubject<ConnectionStatus, Never>(.disconnected)
var connectionStatus: ConnectionStatus {
get { _connectionStatus.value }
}
var connectionStatusPublisher: AnyPublisher<ConnectionStatus, Never> {
_connectionSubject.eraseToAnyPublisher()
}
func connect() {
// 网络连接逻辑
_connectionStatus.send(.connecting)
// ... 连接成功后
_connectionStatus.send(.connected)
}
enum ConnectionStatus {
case disconnected
case connecting
case connected
}
}
// 使用
let networkManager = NetworkManager()
networkManager.connectionStatusPublisher
.sink { status in
print("连接状态: \(status)")
}
.store(in: &cancellables)
5.4.3 使用 PassthroughSubject(不保存当前值)
PassthroughSubject 适合事件类型的属性,不需要保存当前值,只关注变化事件。
基本用法:
class ButtonViewModel {
// 按钮点击事件
let buttonTap = PassthroughSubject<Void, Never>()
// 用户操作事件
let userAction = PassthroughSubject<UserAction, Never>()
enum UserAction {
case login
case logout
case refresh
}
}
// 使用
let viewModel = ButtonViewModel()
viewModel.buttonTap
.sink { print("按钮被点击") }
.store(in: &cancellables)
viewModel.userAction
.sink { action in
switch action {
case .login: print("用户登录")
case .logout: print("用户登出")
case .refresh: print("刷新数据")
}
}
.store(in: &cancellables)
// 触发事件
viewModel.buttonTap.send()
viewModel.userAction.send(.login)
5.4.4 使用 KVO(Key-Value Observing)
对于 NSObject 的子类,可以使用 KVO 将属性转换为 Publisher。
基本用法:
import Combine
class Person: NSObject {
@objc dynamic var name: String = ""
@objc dynamic var age: Int = 0
}
// 使用
let person = Person()
// 将 KVO 属性转换为 Publisher
person.publisher(for: \.name, options: [.initial, .new])
.sink { name in
print("姓名变化: \(name)")
}
.store(in: &cancellables)
person.publisher(for: \.age, options: [.initial, .new])
.sink { age in
print("年龄变化: \(age)")
}
.store(in: &cancellables)
// 修改属性会触发 Publisher
person.name = "张三" // 输出: 姓名变化: 张三
person.age = 25 // 输出: 年龄变化: 25
KVO Options 说明:
.initial:订阅时立即发送当前值.new:属性变化时发送新值.old:属性变化时发送旧值.prior:变化前发送旧值,变化后发送新值
5.4.5 使用 NotificationCenter
将系统通知或自定义通知转换为 Publisher。
基本用法:
// 系统通知
let keyboardWillShow = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.map { notification -> CGRect in
(notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero
}
keyboardWillShow
.sink { frame in
print("键盘高度: \(frame.height)")
}
.store(in: &cancellables)
// 自定义通知
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
static let dataDidUpdate = Notification.Name("dataDidUpdate")
}
let userLoginPublisher = NotificationCenter.default
.publisher(for: .userDidLogin)
.compactMap { $0.userInfo?["user"] as? User }
userLoginPublisher
.sink { user in
print("用户登录: \(user.name)")
}
.store(in: &cancellables)
// 发送通知
NotificationCenter.default.post(
name: .userDidLogin,
object: nil,
userInfo: ["user": currentUser]
)
5.4.6 使用 Timer 将时间属性转换为 Publisher
将定时器转换为 Publisher,用于周期性更新。
基本用法:
class ClockViewModel {
// 方式1:使用 Timer.publish
var currentTime: AnyPublisher<Date, Never> {
Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.eraseToAnyPublisher()
}
// 方式2:创建可控制的定时器
private var timerCancellable: AnyCancellable?
func startTimer() {
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { [weak self] date in
self?.updateTime(date)
}
}
func stopTimer() {
timerCancellable?.cancel()
timerCancellable = nil
}
private func updateTime(_ date: Date) {
// 更新时间
}
}
5.4.7 组合多个属性 Publisher
使用 Combine 操作符组合多个属性 Publisher。
场景1:表单验证
class FormViewModel: ObservableObject {
@Published var username: String = ""
@Published var password: String = ""
@Published var confirmPassword: String = ""
// 组合多个属性,实时验证表单
var isFormValid: AnyPublisher<Bool, Never> {
Publishers.CombineLatest3($username, $password, $confirmPassword)
.map { username, password, confirmPassword in
!username.isEmpty &&
password.count >= 6 &&
password == confirmPassword
}
.eraseToAnyPublisher()
}
// 用户名验证
var usernameValidation: AnyPublisher<String?, Never> {
$username
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.map { username in
if username.isEmpty {
return "用户名不能为空"
} else if username.count < 3 {
return "用户名至少3个字符"
}
return nil
}
.eraseToAnyPublisher()
}
}
场景2:搜索功能
class SearchViewModel: ObservableObject {
@Published var searchText: String = ""
@Published var selectedCategory: String = "all"
@Published var sortOrder: SortOrder = .ascending
enum SortOrder {
case ascending
case descending
}
// 组合多个条件,触发搜索
var searchTrigger: AnyPublisher<(String, String, SortOrder), Never> {
Publishers.CombineLatest3($searchText, $selectedCategory, $sortOrder)
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
private var cancellables = Set<AnyCancellable>()
init() {
searchTrigger
.sink { [weak self] text, category, order in
self?.performSearch(text: text, category: category, order: order)
}
.store(in: &cancellables)
}
private func performSearch(text: String, category: String, order: SortOrder) {
// 执行搜索
}
}
场景3:实时计算属性
class ShoppingCartViewModel: ObservableObject {
@Published var items: [CartItem] = []
@Published var discount: Double = 0.0
@Published var shippingFee: Double = 0.0
// 实时计算总价
var totalPrice: AnyPublisher<Double, Never> {
Publishers.CombineLatest3($items, $discount, $shippingFee)
.map { items, discount, shippingFee in
let subtotal = items.reduce(0) { $0 + $1.price * Double($1.quantity) }
let discounted = subtotal * (1 - discount)
return discounted + shippingFee
}
.eraseToAnyPublisher()
}
// 商品数量变化时自动更新
var itemCount: AnyPublisher<Int, Never> {
$items
.map { $0.reduce(0) { $0 + $1.quantity } }
.eraseToAnyPublisher()
}
}
struct CartItem {
let id: String
var quantity: Int
let price: Double
}
5.4.8 属性转换的最佳实践
1. 选择合适的转换方式
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| ViewModel/ObservableObject | @Published | 与 SwiftUI 集成,自动管理 |
| 需要手动控制发布时机 | CurrentValueSubject | 更灵活的控制 |
| 事件类型(不保存状态) | PassthroughSubject | 只关注事件,不保存值 |
| NSObject 子类 | KVO .publisher(for:) | 利用现有 KVO 机制 |
| 系统通知 | NotificationCenter.publisher | 系统级事件 |
| 定时更新 | Timer.publish | 周期性更新 |
2. 避免内存泄漏
// ✅ 正确:使用 weak self
class ViewModel: ObservableObject {
@Published var data: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$data
.sink { [weak self] value in
self?.processData(value)
}
.store(in: &cancellables)
}
private func processData(_ value: String) {
// 处理数据
}
}
// ❌ 错误:强引用循环
class ViewModel: ObservableObject {
@Published var data: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$data
.sink { [self] value in // 强引用 self
self.processData(value)
}
.store(in: &cancellables)
}
}
3. 使用 dropFirst() 跳过初始值
class ViewModel: ObservableObject {
@Published var searchText: String = ""
init() {
// 跳过初始值,只在用户输入时触发
$searchText
.dropFirst() // 跳过初始的 ""
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.sink { [weak self] text in
self?.performSearch(text)
}
.store(in: &cancellables)
}
}
4. 类型擦除保持接口简洁
class DataManager {
private let _data = CurrentValueSubject<[String], Never>([])
// ✅ 暴露类型擦除的 Publisher
var dataPublisher: AnyPublisher<[String], Never> {
_data.eraseToAnyPublisher()
}
// ❌ 不推荐:直接暴露 CurrentValueSubject
// var dataPublisher: CurrentValueSubject<[String], Never> { _data }
}
5. 组合多个属性的模式
// 模式1:CombineLatest(所有属性都变化时触发)
Publishers.CombineLatest($name, $age)
.sink { name, age in
// name 或 age 任一变化都会触发
}
// 模式2:Zip(需要成对变化)
Publishers.Zip($name, $age)
.sink { name, age in
// name 和 age 必须都变化一次才触发
}
// 模式3:Merge(任一变化时触发)
Publishers.Merge($name.map { "name: \($0)" }, $age.map { "age: \($0)" })
.sink { message in
// name 或 age 变化都会触发
}
6. Schedulers调度器
Schedulers 决定操作在哪个线程执行。
6.1 内置Scheduler
DispatchQueue
// subscribe(on:):订阅与上游工作在哪个调度器;receive(on:):下游收值在哪个调度器(常用主线程更新 UI)
[1, 2, 3].publisher
.subscribe(on: DispatchQueue.global()) // 在后台线程执行订阅与上游
.receive(on: DispatchQueue.main) // 在主线程接收并执行 sink
.sink { print($0) }
RunLoop
// RunLoop 也符合 Scheduler,可在当前 RunLoop 上调度
[1, 2, 3].publisher
.subscribe(on: RunLoop.current)
.sink { print($0) }
OperationQueue
// OperationQueue 可作为 Scheduler,可限制并发数
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
[1, 2, 3, 4, 5].publisher
.subscribe(on: queue)
.sink { print($0) }
6.2 ImmediateScheduler
立即执行,用于测试。
// ImmediateScheduler:不延迟,立即在当前上下文执行,常用于测试
let scheduler = ImmediateScheduler.shared
[1, 2, 3].publisher
.receive(on: scheduler)
.sink { print($0) }
7. 错误处理
7.1 错误类型
// 定义领域错误类型,便于在 Publisher 链中统一处理
enum NetworkError: Error {
case invalidURL
case noData
case decodingError
}
func fetchData() -> AnyPublisher<String, NetworkError> {
// setFailureType:把 Never 等改成指定 Failure 类型;eraseToAnyPublisher 隐藏具体类型
return Just("数据")
.setFailureType(to: NetworkError.self)
.eraseToAnyPublisher()
}
7.2 错误处理策略
// 组合使用:先 catch 兜底、再 retry、最后 replaceError 保证 sink 只收到值
fetchData()
.catch { error -> Just<String> in
return Just("默认数据")
}
.retry(3) // 失败时最多重试 3 次
.replaceError(with: "错误时的默认值") // 若仍失败,发默认值并正常结束
.sink { value in
print(value)
}
8. 内存管理
8.1 AnyCancellable
保存订阅,防止提前释放。
// 订阅返回 Cancellable,不保存会被立即释放导致订阅断开;用 Set 集中管理
class ViewController {
private var cancellables = Set<AnyCancellable>()
func setupBinding() {
$name
.sink { print($0) }
.store(in: &cancellables) // 把订阅存进集合,生命周期与 ViewController 一致
}
}
8.2 Store 内容管理机制
Store 的作用与原理
store(in:) 方法是 Combine 中管理订阅生命周期的核心机制。理解其工作原理对于正确使用 Combine 至关重要。
8.2.1 AnyCancellable 的本质
// AnyCancellable 是类型擦除的 Cancellable 包装器
public struct AnyCancellable: Cancellable, Hashable {
private let _cancel: () -> Void
public init(_ cancel: @escaping () -> Void) {
self._cancel = cancel
}
public func cancel() {
_cancel()
}
// 当 AnyCancellable 被释放时,自动调用 cancel()
deinit {
cancel()
}
}
关键特性:
AnyCancellable是值类型(struct),但内部持有取消操作的闭包- 当
AnyCancellable实例被释放时,会自动调用cancel()方法 - 这确保了订阅在持有者释放时能够正确清理
8.2.2 store(in:) 方法的工作原理
extension Cancellable {
/// 将 Cancellable 存储到 Set 中,延长其生命周期
public func store(in set: inout Set<AnyCancellable>) {
set.insert(AnyCancellable(self))
}
/// 将 Cancellable 存储到 AnyCancellable 中(单个订阅场景)
public func store(in cancellable: inout AnyCancellable?) {
cancellable = AnyCancellable(self)
}
}
工作流程:
1. 调用 .sink(...) 或 .assign(...) 返回 Cancellable
↓
2. 调用 .store(in: &cancellables)
↓
3. 将 Cancellable 包装成 AnyCancellable
↓
4. 插入到 Set<AnyCancellable> 中
↓
5. Set 持有 AnyCancellable,延长订阅生命周期
↓
6. 当对象(如 ViewController)释放时,Set 也被释放
↓
7. Set 中所有 AnyCancellable 的 deinit 被调用
↓
8. 每个 AnyCancellable 的 cancel() 被调用
↓
9. 订阅被取消,资源被清理
8.2.3 Set<AnyCancellable> 的管理策略
为什么使用 Set?
// Set 的优势:
// 1. 自动去重(AnyCancellable 实现了 Hashable)
// 2. 高效的插入和查找
// 3. 批量管理多个订阅
class ViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
@Published var email: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
// 多个订阅可以统一管理
$name
.sink { print("Name: \($0)") }
.store(in: &cancellables)
$age
.sink { print("Age: \($0)") }
.store(in: &cancellables)
$email
.sink { print("Email: \($0)") }
.store(in: &cancellables)
}
// 当 ViewModel 释放时,所有订阅自动取消
}
生命周期管理示例:
class ViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
func setupBindings() {
// 订阅1:监听数据变化
viewModel.$data
.receive(on: DispatchQueue.main)
.sink { [weak self] data in
self?.updateUI(with: data)
}
.store(in: &cancellables)
// 订阅2:监听错误
viewModel.$error
.compactMap { $0 }
.sink { [weak self] error in
self?.showError(error)
}
.store(in: &cancellables)
// 订阅3:网络请求
viewModel.fetchData()
.sink(
receiveCompletion: { [weak self] completion in
if case .failure(let error) = completion {
self?.handleError(error)
}
},
receiveValue: { [weak self] data in
self?.handleData(data)
}
)
.store(in: &cancellables)
}
// 当 ViewController 被释放时:
// 1. cancellables Set 被释放
// 2. Set 中所有 AnyCancellable 的 deinit 被调用
// 3. 所有订阅自动取消,避免内存泄漏
}
8.2.4 手动管理 vs 自动管理
手动管理(不推荐):
class ViewController: UIViewController {
private var cancellable: AnyCancellable?
func setupBinding() {
// 需要手动保存,容易忘记
cancellable = $name
.sink { print($0) }
// 如果忘记保存,订阅会立即被释放
$age
.sink { print($0) } // ❌ 立即释放,不会收到任何值
}
// 需要手动取消
deinit {
cancellable?.cancel()
}
}
自动管理(推荐):
class ViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
func setupBinding() {
// 自动管理,无需手动 cancel
$name
.sink { print($0) }
.store(in: &cancellables)
$age
.sink { print($0) }
.store(in: &cancellables)
// 对象释放时自动清理所有订阅
}
}
8.2.5 条件性订阅管理
场景:需要动态添加/移除订阅
class ViewModel: ObservableObject {
@Published var isEnabled: Bool = false
@Published var data: String = ""
private var cancellables = Set<AnyCancellable>()
private var dataSubscription: AnyCancellable?
init() {
// 监听启用状态,动态管理数据订阅
$isEnabled
.sink { [weak self] enabled in
if enabled {
self?.startDataSubscription()
} else {
self?.stopDataSubscription()
}
}
.store(in: &cancellables)
}
private func startDataSubscription() {
// 创建新的订阅
dataSubscription = $data
.sink { print("Data: \($0)") }
// 手动管理单个订阅
// 注意:这里不使用 store(in: &cancellables),因为需要单独控制
}
private func stopDataSubscription() {
// 手动取消订阅
dataSubscription?.cancel()
dataSubscription = nil
}
}
更好的方式:使用条件操作符
class ViewModel: ObservableObject {
@Published var isEnabled: Bool = false
@Published var data: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
// 使用 filter 或 flatMap 实现条件订阅,统一管理
$isEnabled
.filter { $0 } // 只在启用时继续
.flatMap { [weak self] _ -> AnyPublisher<String, Never> in
guard let self = self else {
return Empty().eraseToAnyPublisher()
}
return self.$data.eraseToAnyPublisher()
}
.sink { print("Data: \($0)") }
.store(in: &cancellables)
}
}
8.2.6 Store 的最佳实践
1. 统一管理位置
class ViewModel: ObservableObject {
// ✅ 推荐:在类的顶部声明,统一管理
private var cancellables = Set<AnyCancellable>()
init() {
setupSubscriptions()
}
private func setupSubscriptions() {
// 所有订阅都在这里设置
setupDataSubscription()
setupErrorSubscription()
}
}
2. 避免在闭包中创建新的 Set
// ❌ 错误:每次调用都创建新的 Set
func loadData() {
var cancellables = Set<AnyCancellable>()
API.fetchData()
.sink { }
.store(in: &cancellables) // 函数返回后立即释放
}
// ✅ 正确:使用实例属性
class ViewModel {
private var cancellables = Set<AnyCancellable>()
func loadData() {
API.fetchData()
.sink { }
.store(in: &cancellables) // 生命周期与 ViewModel 一致
}
}
3. 在 SwiftUI 中的使用
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text(viewModel.text)
.onAppear {
// SwiftUI 中,使用 @StateObject 或 @ObservedObject
// 订阅会自动管理,但也可以手动管理
viewModel.$text
.sink { print($0) }
.store(in: &viewModel.cancellables)
}
}
}
class ViewModel: ObservableObject {
@Published var text: String = ""
var cancellables = Set<AnyCancellable>() // 注意:在 SwiftUI 中可能需要 internal
}
4. 测试中的管理
class ViewModelTests: XCTestCase {
func testSubscription() {
let viewModel = ViewModel()
var cancellables = Set<AnyCancellable>()
var receivedValues: [String] = []
viewModel.$data
.sink { receivedValues.append($0) }
.store(in: &cancellables)
viewModel.data = "test"
// 测试完成后,cancellables 会自动清理
XCTAssertEqual(receivedValues, ["test"])
}
}
8.2.7 Store 的内部实现细节
AnyCancellable 的 Hashable 实现:
extension AnyCancellable: Hashable {
public func hash(into hasher: inout Hasher) {
// 使用对象标识符(ObjectIdentifier)作为哈希值
// 这确保了每个 AnyCancellable 实例都是唯一的
hasher.combine(ObjectIdentifier(self))
}
public static func == (lhs: AnyCancellable, rhs: AnyCancellable) -> Bool {
// 使用对象标识符比较
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
为什么 Set 可以自动去重:
// 每个 AnyCancellable 实例都有唯一的对象标识符
// 即使包装相同的 Cancellable,也是不同的 AnyCancellable 实例
let cancellable1 = publisher.sink { }
let cancellable2 = publisher.sink { }
var set = Set<AnyCancellable>()
set.insert(AnyCancellable(cancellable1)) // 插入成功
set.insert(AnyCancellable(cancellable2)) // 插入成功(不同的实例)
// 但如果尝试插入相同的 AnyCancellable:
let anyCancellable = AnyCancellable(cancellable1)
set.insert(anyCancellable) // 插入成功
set.insert(anyCancellable) // 插入失败(已存在)
8.2.8 常见错误与解决方案
错误1:忘记 store
// ❌ 错误:订阅立即被释放
func setupBinding() {
$name.sink { print($0) } // 立即释放,不会收到任何值
}
// ✅ 正确:使用 store
func setupBinding() {
$name
.sink { print($0) }
.store(in: &cancellables)
}
错误2:在局部作用域中 store
// ❌ 错误:函数返回后 Set 被释放
func loadData() {
var cancellables = Set<AnyCancellable>()
API.fetchData()
.sink { }
.store(in: &cancellables)
// 函数返回后,cancellables 被释放,订阅被取消
}
// ✅ 正确:使用实例属性
class ViewModel {
private var cancellables = Set<AnyCancellable>()
func loadData() {
API.fetchData()
.sink { }
.store(in: &cancellables)
}
}
错误3:循环引用导致无法释放
// ❌ 错误:强引用循环
class ViewModel {
private var cancellables = Set<AnyCancellable>()
func setup() {
$data.sink { [self] value in // 强引用 self
self.process(value)
}
.store(in: &cancellables)
// self → cancellables → AnyCancellable → 闭包 → self(循环)
}
}
// ✅ 正确:使用 weak self
class ViewModel {
private var cancellables = Set<AnyCancellable>()
func setup() {
$data.sink { [weak self] value in // 弱引用
self?.process(value)
}
.store(in: &cancellables)
}
}
8.3 避免循环引用
// 在 sink 里用到 self 时用 [weak self],避免 self → cancellables → 闭包 → self 的循环
class ViewModel {
@Published var data: String = ""
func setup() {
$data
.sink { [weak self] value in
self?.process(value)
}
.store(in: &cancellables)
}
private func process(_ value: String) {
// 处理数据
}
private var cancellables = Set<AnyCancellable>()
}
9. 实际应用场景
9.1 网络请求
// 使用 dataTaskPublisher 将请求转为 Publisher,再 map/decode 成模型
struct API {
static func fetchUser(id: Int) -> AnyPublisher<User, Error> {
let url = URL(string: "https://api.example.com/users/\(id)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
API.fetchUser(id: 1)
.receive(on: DispatchQueue.main) // 回到主线程再更新 UI
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("错误: \(error)")
}
},
receiveValue: { user in
print("用户: \(user)")
}
)
.store(in: &cancellables)
9.2 用户输入处理
// 搜索框:防抖 + 去重 + 非空过滤 + flatMap 发请求,结果用 assign 写回 @Published
class SearchViewModel: ObservableObject {
@Published var searchText: String = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.flatMap { query -> AnyPublisher<[String], Never> in
return self.search(query: query)
.catch { _ in Just([]) } // 失败时给空数组,保持 Never
.eraseToAnyPublisher()
}
.assign(to: \.results, on: self)
.store(in: &cancellables)
}
private func search(query: String) -> AnyPublisher<[String], Error> {
return Just(["结果1", "结果2"])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
9.3 组合多个数据源
// Zip 等两个请求都完成后再一起处理,适合「同时拉用户与帖子」再更新 UI
class DashboardViewModel: ObservableObject {
@Published var user: User?
@Published var posts: [Post] = []
@Published var isLoading: Bool = false
private var cancellables = Set<AnyCancellable>()
func loadData() {
isLoading = true
let userPublisher = API.fetchUser(id: 1)
let postsPublisher = API.fetchPosts()
Publishers.Zip(userPublisher, postsPublisher)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
print("错误: \(error)")
}
},
receiveValue: { [weak self] user, posts in
self?.user = user
self?.posts = posts
}
)
.store(in: &cancellables)
}
}
10. 更多使用案例
10.1 表单验证(多字段实时校验)
// 用 map 生成错误文案 / 是否有效,assign 到 @Published,实现实时校验
class FormViewModel: ObservableObject {
@Published var username: String = ""
@Published var password: String = ""
@Published var confirmPassword: String = ""
@Published var isFormValid: Bool = false
@Published var usernameError: String?
@Published var passwordError: String?
private var cancellables = Set<AnyCancellable>()
init() {
// 用户名:非空 + 长度,错误信息写回 usernameError
$username
.map { name in
if name.isEmpty { return "请输入用户名" }
if name.count < 3 { return "至少 3 个字符" }
return nil
}
.assign(to: \.usernameError, on: self)
.store(in: &cancellables)
// 三字段 combineLatest,任一变化都重新计算表单是否有效
Publishers.CombineLatest3($username, $password, $confirmPassword)
.map { name, pwd, confirm in
if name.isEmpty || pwd.isEmpty { return false }
if pwd != confirm { return false }
if pwd.count < 6 { return false }
return true
}
.assign(to: \.isFormValid, on: self)
.store(in: &cancellables)
}
}
10.2 NotificationCenter 转 Publisher
// 系统通知转成 Publisher,再 map 出需要的 payload(如键盘 frame)
let keyboardWillShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.map { notification in
(notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero
}
.receive(on: DispatchQueue.main)
keyboardWillShow
.sink { frame in
print("键盘高度: \(frame.height)")
}
.store(in: &cancellables)
// 自定义通知名同样用 publisher(for:)
extension Notification.Name {
static let myCustomEvent = Notification.Name("MyCustomEvent")
}
let customPublisher = NotificationCenter.default.publisher(for: .myCustomEvent)
10.3 Timer 与周期任务
// Timer.publish + autoconnect:按间隔持续发当前日期,需手动 cancel 停止
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { date in
print("tick: \(date)")
}
// 5 秒后断开
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
timerPublisher.cancel()
}
// 或用 delay + flatMap 递归实现「间隔重复任务」
func repeatingTask(interval: TimeInterval) -> AnyPublisher<Date, Never> {
Just(Date())
.delay(for: .seconds(interval), scheduler: DispatchQueue.main)
.flatMap { _ in repeatingTask(interval: interval) }
.eraseToAnyPublisher()
}
10.4 SwiftUI 与 @Published 深度绑定
// @Published 变化时同步到 UserDefaults;dropFirst 避免 init 时的初始值触发写入
class SettingsViewModel: ObservableObject {
@Published var isDarkMode: Bool = false
@Published var fontSize: Double = 14
private var cancellables = Set<AnyCancellable>()
init() {
$isDarkMode
.dropFirst()
.sink { UserDefaults.standard.set($0, forKey: "darkMode") }
.store(in: &cancellables)
$fontSize
.dropFirst()
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink { UserDefaults.standard.set($0, forKey: "fontSize") }
.store(in: &cancellables)
}
}
// SwiftUI 通过 @ObservedObject 与 $ 绑定,自动刷新
struct SettingsView: View {
@ObservedObject var viewModel: SettingsViewModel
var body: some View {
Toggle("深色模式", isOn: $viewModel.isDarkMode)
Slider(value: $viewModel.fontSize, in: 10...24)
}
}
10.5 多源竞速(先到先用)
// 主源失败时用 catch 切到备用源,实现主/备切换
func loadFromPrimaryOrFallback() -> AnyPublisher<Data, Error> {
let primary = URLSession.shared.dataTaskPublisher(for: primaryURL)
.map(\.data)
.mapError { $0 as Error }
let fallback = URLSession.shared.dataTaskPublisher(for: fallbackURL)
.map(\.data)
.mapError { $0 as Error }
return primary
.catch { _ in fallback }
.eraseToAnyPublisher()
}
// 显式 race:merge 后取 first(),即「谁先完成用谁」
extension Publishers {
static func race<A: Publisher, B: Publisher>(_ a: A, _ b: B) -> AnyPublisher<A.Output, A.Failure>
where A.Output == B.Output, A.Failure == B.Failure {
a.merge(with: b)
.first()
.eraseToAnyPublisher()
}
}
10.6 KVO 替代(观察对象属性)
// NSObject + @objc dynamic 可用 .publisher(for:options:) 转成 Combine 流,替代 KVO
class Person: NSObject {
@objc dynamic var name: String = ""
}
let person = Person()
let namePublisher = person.publisher(for: \.name, options: [.initial, .new])
.compactMap { $0 as? String }
.sink { print("name: \($0)") }
person.name = "张三" // 输出: name: 张三
10.7 请求重试与超时
// timeout:超时未完成则发失败;retry + catch 实现重试与最终兜底
URLSession.shared.dataTaskPublisher(for: url)
.timeout(.seconds(10), scheduler: DispatchQueue.main)
.retry(3)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.catch { error -> Just<User> in
return Just(User.placeholder)
}
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { user in
// 更新 UI
}
)
.store(in: &cancellables)
10.8 节流与防抖组合(搜索 + 连续点击)
// 搜索:防抖,避免每次按键都请求;失败时用 catch 给空数组
$searchText
.debounce(for: .milliseconds(400), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap { query in
searchAPI(query: query).catch { _ in Just([]) }.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.assign(to: \.results, on: self)
.store(in: &cancellables)
// 按钮:节流 1 秒内只响应一次,防止重复提交
buttonTapPublisher
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: false)
.sink { submit() }
.store(in: &cancellables)