Combine 内置的
PassthroughSubject和CurrentValueSubject覆盖了大多数事件传递与状态管理场景。但当你的需求超出"直通"和"记忆"时------比如你希望发送的值必须通过验证、在一定时间内去重、或者自动聚合------自定义 Subject 便能让你将这些逻辑封装在可复用的发布者内部,让外部调用者依然以熟悉的.send()方式交付数据,而无需在每个订阅点重复过滤逻辑。
本章将带你理解 Subject 协议的本质,学习如何基于现有 Subject 快速定制出符合业务需求的专属事件源,并给出多个可直接投入生产的实战案例。
1. Subject 协议:一半 Publisher,一半指令
Subject 是一个协议,它同时继承了 Publisher 和 AnyObject。这意味着 Subject 必然是引用类型(class)。它额外要求实现两个方法:
swift
public protocol Subject: AnyObject, Publisher {
func send(_ value: Self.Output)
func send(completion: Subscribers.Completion<Self.Failure>)
}
这就赋予了 Subject 双重身份:
- 作为 Publisher:可以被任意数量的订阅者订阅。
- 作为 指令入口 :外部代码可以主动调用
send()向所有订阅者广播值或完成事件。
Combine 内置的两个 Subject ------ PassthroughSubject(不保存历史值)和 CurrentValueSubject(保存并暴露最新值)------ 已经实现了这套机制。自定义 Subject 通常就是在这两个内建类的之上包装一层逻辑。
2. 为什么需要自定义 Subject
你可能会问:既然操作符如 filter、debounce、removeDuplicates 都能在管道中完成同样的功能,为什么要把逻辑塞进 Subject 里?
场景驱动答案:
- 可复用的事件入口 :当你有一个全局事件总线(Event Bus),希望所有
.send()进入的值自动跳过连续重复,就可以用DistinctSubject直接替换PassthroughSubject,而不必在每个订阅处重复书写removeDuplicates()。 - 业务规则强制:例如一个"订单提交 Subject",必须保证提交的订单金额大于 0,否则直接丢弃。这个规则应该内聚在 Subject 内部,防止调用方忘记校验。
- 性能优化 :在高频事件(如加速度计数据)中,如果发送方不加以节制,下游即使使用
throttle也可能被淹没。在 Subject 层直接限流可以减轻整体管道压力。 - 清晰度与一致性 :将定制逻辑放在 Subject 中,让数据源的定义更加语义化。一个
DebouncedTextSubject一看就知道它会自动防抖,不必每次在多个管道中复制相同的防抖配置。
3. 自定义 Subject 的通用模式
自定义 Subject 并不需要你重新实现底层的订阅、背压、线程安全等复杂机制。标准做法是在内部持有一个内建 Subject(通常是 PassthroughSubject 或 CurrentValueSubject) ,将所有订阅转发给它,同时在 send() 方法中插入自己的处理逻辑。
以下是一个空白的自定义 Subject 骨架:
swift
class CustomSubject<Output, Failure: Error>: Subject {
// 内部真正负责发布的对象
private let innerSubject: PassthroughSubject<Output, Failure>
init() {
innerSubject = PassthroughSubject()
}
// 转发 send
func send(_ value: Output) {
// 在这里加入自定义逻辑
// ...
innerSubject.send(value)
}
// 转发 completion
func send(completion: Subscribers.Completion<Failure>) {
innerSubject.send(completion: completion)
}
// 转发订阅
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
innerSubject.receive(subscriber: subscriber)
}
}
要点:
- 必须实现
receive(subscriber:)将订阅者桥接到内部 Subject。 send(_:)和send(completion:)是外部调用入口,也是加工逻辑的注入点。- 内部 Subject 负责所有线程安全与背压细节,你只需聚焦业务规则。
4. 实战案例一:带验证的 Subject
在许多表单场景中,需要保证输入值满足特定条件才能被接受。ValidatedSubject 在 send 时执行闭包验证,拒绝非法值。
swift
class ValidatedSubject<Output, Failure: Error>: Subject {
private let subject = PassthroughSubject<Output, Failure>()
private let validator: (Output) -> Bool
init(validator: @escaping (Output) -> Bool) {
self.validator = validator
}
func send(_ value: Output) {
guard validator(value) else { return } // 验证不通过,直接丢弃
subject.send(value)
}
func send(completion: Subscribers.Completion<Failure>) {
subject.send(completion: completion)
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
使用示例:
swift
let nameSubject = ValidatedSubject<String, Never> { name in
name.count >= 3 && name.count <= 20
}
nameSubject.sink { print("✅ \($0)") }
.store(in: &cancellables)
nameSubject.send("AB") // 被丢弃
nameSubject.send("Alice") // ✅ Alice
nameSubject.send("Long name exceeding twenty chars") // 被丢弃
5. 实战案例二:防抖 Subject
在搜索框场景中,我们希望仅当用户停止输入一段时间后才发送最终值。虽然可以用操作符 .debounce,但若多个 ViewModel 都需要同样的防抖行为,一个 DebouncedSubject 可以直接作为数据源。
swift
class DebouncedSubject<Output, Failure: Error>: Subject {
private let subject = PassthroughSubject<Output, Failure>()
private var workItem: DispatchWorkItem?
private let dueTime: DispatchTimeInterval
private let queue: DispatchQueue
init(dueTime: DispatchTimeInterval = .milliseconds(300),
queue: DispatchQueue = .main) {
self.dueTime = dueTime
self.queue = queue
}
func send(_ value: Output) {
workItem?.cancel()
let item = DispatchWorkItem { [weak self] in
self?.subject.send(value)
}
workItem = item
queue.asyncAfter(deadline: .now() + dueTime, execute: item)
}
func send(completion: Subscribers.Completion<Failure>) {
workItem?.cancel()
subject.send(completion: completion)
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
使用示例:
swift
let searchSubject = DebouncedSubject<String, Never>(dueTime: .milliseconds(500))
searchSubject.sink { print("搜索: \($0)") }
.store(in: &cancellables)
searchSubject.send("S")
searchSubject.send("Sw")
searchSubject.send("Swi")
searchSubject.send("Swift") // 仅此值将在 0.5 秒后被发送
6. 实战案例三:去除连续重复值的 Subject
DistinctSubject 比较相邻两个值,若相同则拒绝发送。内部可以基于 Equatable 或自定义比较闭包。
swift
class DistinctSubject<Output: Equatable, Failure: Error>: Subject {
private let subject = PassthroughSubject<Output, Failure>()
private var lastValue: Output?
func send(_ value: Output) {
guard value != lastValue else { return }
lastValue = value
subject.send(value)
}
func send(completion: Subscribers.Completion<Failure>) {
subject.send(completion: completion)
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
7. 实战案例四:限流 Subject
在高频数据源(如传感器)中,可能需要强制最小发送间隔。ThrottledSubject 记录上次发送时间,若距上次不足指定间隔则丢弃当前值。
swift
class ThrottledSubject<Output, Failure: Error>: Subject {
private let subject = PassthroughSubject<Output, Failure>()
private var lastSendTime: Date = .distantPast
private let minimumInterval: TimeInterval
init(minimumInterval: TimeInterval) {
self.minimumInterval = minimumInterval
}
func send(_ value: Output) {
let now = Date()
guard now.timeIntervalSince(lastSendTime) >= minimumInterval else { return }
lastSendTime = now
subject.send(value)
}
func send(completion: Subscribers.Completion<Failure>) {
subject.send(completion: completion)
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
8. 实战案例五:聚合 Subject
某些场景下,希望收集一定数量的值后进行一次聚合运算再发出。AggregatingSubject 内部维护缓冲区,达到阈值时调用聚合闭包产生一个结果并清空缓冲区。
swift
class AggregatingSubject<Output, Failure: Error>: Subject {
private let subject = PassthroughSubject<Output, Failure>()
private let aggregation: ([Output]) -> Output
private let maxBufferSize: Int
private var buffer: [Output] = []
init(maxBufferSize: Int, aggregation: @escaping ([Output]) -> Output) {
self.maxBufferSize = maxBufferSize
self.aggregation = aggregation
}
func send(_ value: Output) {
buffer.append(value)
if buffer.count >= maxBufferSize {
flush()
}
}
func send(completion: Subscribers.Completion<Failure>) {
flush() // 确保最后残留的值也被聚合
subject.send(completion: completion)
}
private func flush() {
guard !buffer.isEmpty else { return }
let result = aggregation(buffer)
buffer.removeAll()
subject.send(result)
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
使用:
swift
let sumSubject = AggregatingSubject<Int, Never>(maxBufferSize: 3, aggregation: { $0.reduce(0, +) })
sumSubject.sink { print("聚合和: \($0)") }
.store(in: &cancellables)
sumSubject.send(1) // 缓冲
sumSubject.send(2) // 缓冲
sumSubject.send(3) // 输出: 聚合和: 6 (1+2+3)
sumSubject.send(4) // 缓冲
sumSubject.send(5) // 缓冲(未满,暂不输出)
// 手动完成或超过阈值才会输出下一批
9. 最佳实践
- 简单优先:如果内置 Subject 或常规操作符就能满足需求,不要自定义 Subject。自定义 Subject 适用于需要强制封装或高频复用的业务规则。
- 线程安全 :自定义 Subject 内部通常依赖一个串行的 PassthroughSubject,它本身是线程安全的。但如果你在
send中添加了额外共享可变状态(如缓冲区、计时器),务必保证这些状态的访问在同一队列或使用锁保护。 - 内存管理 :Subject 是引用类型,需注意避免循环引用。当 Subject 持有外部闭包时,使用
[weak self]打破循环。 - 完成的传递 :当外部调用
send(completion:)时,通常应刷新内部缓冲区(如果有),再向下游传递完成事件。 - 定制与测试:自定义 Subject 应该能像普通 Subject 一样在单元测试中通过发送值来验证行为。这使得业务规则的单元测试变得极为简单。
- 避免重新发明轮子 :大部分时间可以通过组合操作符 (
debounce,throttle,removeDuplicates,buffer等) 实现类似功能。只有当"主动发送"的入口也需要这些限制时,才必须用自定义 Subject。
10. 总结
Combine 的 Subject 协议为开发者打开了高度定制的事件入口。通过将一个内建 Subject 包裹在自定义类中,我们可以赋予 send 方法验证、限流、防抖、去重甚至聚合能力,从而在数据源头就应用业务约束。这不仅让调用方免于编写重复的过滤逻辑,更通过封装提升了代码的语义和可维护性。
掌握自定义 Subject 的编写,意味着你可以将任何复杂的发布规则凝固成可复用的组件,让 Combine 管道的前端和后端一样优雅、可控。