Combine 框架详解:发布者、订阅者与操作符
Combine 是 Apple 推出的一个声明式函数响应式编程框架,用于处理事件流和异步操作。
核心概念:Publisher, Operator, Subscriber
Combine 的核心围绕三个主要协议展开:Publisher、Operator 和 Subscriber。
Publisher (发布者)
Publisher 负责生成并发布数据。它是一个协议,其类型由发布的数据(Output)和可能发生的错误(Failure)共同定义,表示为 Publisher。发布者可以发出零个、一个或多个数据,并最终以正常完成或出错的方式结束。
Operator (操作符)
Operator 用于响应和转换数据流。操作符接收特定类型的数据和错误作为输入 (Input, Failure),并产生另一组类型的数据和错误作为输出 (Output, Failure)。操作符可以链式调用,对数据流进行一系列的加工和处理。
Subscriber (订阅者)
Subscriber 用于接收和处理来自发布者的数据。它是一个协议,其类型由接收的数据(Input)和可能发生的错误(Failure)定义,表示为 Subscriber。订阅者通过 subscribe(_:) 方法与发布者建立连接。
一个典型的 Combine 数据流示例如下:
// Publisher: Just 发出单个值
Just(10)
// Operator: map 将整数转换为字符串
.map { String($0) }
// Subscriber: sink 处理最终接收到的值
.sink { receivedString in
print("最终结果是: \(receivedString)")
}
Subject (主题)
Subject 是一种特殊的 Publisher,它本身也遵循 Publisher 协议,并额外提供了一个 send(_:) 方法来主动发送数据。Combine 提供了两个常用的 Subject 实现:PassthroughSubject 和 CurrentValueSubject。
- PassthroughSubject: 这种 Subject 没有初始值,也不会存储当前状态。订阅者只有在订阅之后才能接收到它发送的数据。可以将其比作一个门铃,没有固定的状态,只有当有人按门铃(发送数据)时,在家的人(订阅者)才能听到。
- CurrentValueSubject: 这种 Subject 包含一个初始值,并始终保持当前状态。订阅者在订阅时会立即收到当前的最新值,之后再接收新的值。可以将其比作一个电灯,有开/关的状态,新回家的人(订阅者)可以立即看到当前是开还是关,然后会收到状态变化通知。
以下是 PassthroughSubject 的示例:
let messageRelay = PassthroughSubject<String, Never>()
let subscription1 = messageRelay.sink { message in
print("订阅者1收到消息: \(message)")
}
messageRelay.send("你好")
messageRelay.send("世界!")
// 输出:
// 订阅者1收到消息: 你好
// 订阅者1收到消息: 世界!
以下是 CurrentValueSubject 的示例:
let textEditor = CurrentValueSubject<String, Never>("")
textEditor.send("初始文本")
let subscription2 = textEditor.sink { currentText in
print("订阅者2收到文本: \(currentText)")
}
textEditor.send("更多文本")
// 输出:
// 订阅者2收到文本: 初始文本
// 订阅者2收到文本: 更多文本
Cancellable (可取消订阅)
在 Combine 中,我们通常使用 sink 或 assign 作为订阅者。sink 使用闭包来处理接收到的值,而 assign 则可以将接收到的值赋给对象的某个属性。
assign(to:on:): 将发布者的值赋给指定对象的某个属性。assign(to:): 将发布者的值赋给另一个 Publisher。
sink 和 assign(to:on:) 方法都会返回一个 Cancellable 协议类型的对象。通过调用 Cancellable 对象的 cancel() 方法,可以手动停止接收数据,从而取消订阅。
示例代码:
// 假设 publishingSource 是一个 Publisher
let streamCancellable = publishingSource.sink { value in
print("Sink 接收到值: \(value)")
}
// streamCancellable.cancel() // 取消订阅
// 假设 yourButton 是一个 UIButton 实例
let buttonCancellable = publishingSource
.receive(on: RunLoop.main) // 确保在主线程更新 UI
.assign(to: \.isEnabled, on: yourButton)
// buttonCancellable.cancel() // 取消订阅
// 将一个 publisher 的值赋给另一个 publisher
let assignmentCancellable = publishingSource.assign(to: &anotherPublisher)
AnyPublisher 和 AnyCancellable
AnyPublisher 和 AnyCancellable 是类型擦除(Type Erasure)的包装器,用于隐藏具体的 Publisher 和 Cancellable 类型,方便统一处理。
- AnyPublisher: 任何遵循
Publisher协议的类型都可以通过eraseToAnyPublisher()方法转换为AnyPublisher。这有助于在代码中统一处理不同类型的发布者,并隐藏其底层实现细节。 - AnyCancellable:
sink和assign方法返回的Cancellable对象实际上是AnyCancellable的实例。AnyCancellable必须被妥善存储(例如,保存在一个Set中),否则当它离开作用域时会被自动销毁,导致订阅失效。
使用 AnyPublisher 和存储 AnyCancellable 的示例:
let dataProvider: AnyPublisher<String, Never> = ["数据1", "数据2", "数据3"].publisher
.eraseToAnyPublisher()
var activeSubscriptions = Set<AnyCancellable>()
dataProvider.sink { receivedData in
print("收到数据: \(receivedData)")
}
.store(in: &activeSubscriptions) // 将 AnyCancellable 存储在 Set 中
// 输出:
// 收到数据: 数据1
// 收到数据: 数据2
// 收到数据: 数据3
@Published 属性包装器
@Published 是一个属性包装器,可以为类属性自动提供 Publisher 功能。当一个属性被 @Published 修饰时,它会创建一个对应的 Publisher,并且每次属性值发生变化时,这个 Publisher 都会发出新的值。
如果类的一个属性 value 被 @Published 修饰,那么 $value 就会是一个 Published<Value>.Publisher 类型的 Publisher。
@Published 的使用示例:
class UserProfile {
@Published var userName: String
init(name: String) {
self.userName = name
}
}
let profile = UserProfile(name: "Alice")
// $profile.userName 是一个 Published<String>.Publisher
let profileSubscription = profile.$userName.sink { newName in
print("用户名已更新为: \(newName)")
}
profile.userName = "Bob"
// 输出:
// 用户名已更新为: Bob