什么是 Publisher?
Publisher 是 Combine 框架的核心协议,它定义了一件事:这个对象会随时间推移,向外发出一系列事件。
public protocol Publisher {
associatedtype Output // 发布的数据类型
associatedtype Failure: Error // 可能出现的错误类型
func receive<S>(subscriber: S) where
S: Subscriber,
Self.Failure == S.Failure,
Self.Output == S.Input
}
Publisher 能发出三种事件,后两种一旦发出,事件流永远终止:
| 事件 | 含义 | 能发出几次 |
|---|---|---|
output | 一个新的值 | 零次到无数次 |
failure | 发生了错误 | 最多一次,之后终止 |
finished | 所有值发送完毕 | 最多一次,之后终止 |
实际场景一:UI — 搜索框输入
这是日常开发中最常遇到 Publisher 的场景之一。用户在搜索框里打字,每次击键都是一个新的事件。
不用 Combine 的写法(Delegate)
// ViewController 要实现 UISearchBarDelegate
extension SearchViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// 用户每次输入都触发这里
// 如果要防抖(debounce),还得自己手动管理 Timer
fetchResults(query: searchText)
}
}
问题:防抖、取消上一次请求、多个输入源合并……每一个都要自己手动写。
用 Combine 的写法
import Combine
class SearchViewController: UIViewController {
@IBOutlet weak var searchBar: UISearchBar!
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// searchBar.textDidChangePublisher 就是一个 Publisher
// 它的 Output 是 String,Failure 是 Never(不会出错)
searchBar.textDidChangePublisher // ← 这就是 Publisher
.debounce(for: .milliseconds(300), scheduler: RunLoop.main) // 防抖:停止输入 300ms 后才发出
.removeDuplicates() // 过滤掉和上次一样的内容
.sink { [weak self] searchText in // 接上订阅者,数据才开始流动
self?.fetchResults(query: searchText)
}
.store(in: &cancellables) // 保存订阅,防止立刻被销毁
}
}
// 💡 .sink 和 .store 是 Combine 三巨头中 Subscriber 的核心概念,
// 这里先知道"接收数据用 sink,保活用 store"即可,
// 详细原理会在《三巨头之 Subscriber》中展开。
💡
searchBar.textDidChangePublisher就是一个现成的 Publisher。每次用户输入,它就发出一个新的String值(output)。用户不输入了,它既不 failure 也不 finished,就静静等着。
事件流长这样:
用户输入:s → sw → swi → swift
时间轴:
── "s" ── "sw" ── "swi" ── "swift" ──────────▶
(用户停了,等待下一次输入)
实际场景二:网络请求 — 调用 API
网络请求是 Publisher 的另一个典型场景。请求会发出一个值(返回的数据),然后结束;或者发出一个错误(网络超时、404 等),然后终止。
不用 Combine 的写法(回调)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
let url = URL(string: "https://api.example.com/users/(id)")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else { return }
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(error))
}
}.resume()
}
嵌套回调、错误处理分散、取消请求需要额外管理……
用 Combine 的写法
import Combine
struct User: Decodable {
let id: Int
let name: String
}
class UserViewModel: ObservableObject {
@Published var user: User? = nil
@Published var errorMessage: String? = nil
private var cancellables = Set<AnyCancellable>()
func fetchUser(id: Int) {
let url = URL(string: "https://api.example.com/users/(id)")!
// URLSession.shared.dataTaskPublisher(for:) 返回的就是一个 Publisher
// Output: (data: Data, response: URLResponse)
// Failure: URLError
URLSession.shared.dataTaskPublisher(for: url) // ← 这就是 Publisher
.map(.data) // 只取 data 部分
.decode(type: User.self, decoder: JSONDecoder()) // 解码成 User
.receive(on: DispatchQueue.main) // 切回主线程
.sink(
receiveCompletion: { [weak self] completion in
switch completion {
case .failure(let error):
self?.errorMessage = error.localizedDescription // failure 事件
case .finished:
break // finished 事件,正常结束
}
},
receiveValue: { [weak self] user in
self?.user = user // output 事件:拿到了 User
}
)
.store(in: &cancellables)
}
}
💡
dataTaskPublisher是 URLSession 内置的 Publisher。它最多发出一个 output(请求成功的数据),然后发出 finished;如果网络出错,则发出 failure 然后终止。
事件流长这样:
正常情况(请求成功):
── (data, response) ── finished ✓
出错情况(网络超时):
── failure(URLError.timedOut) ✗
两个场景的对比
| UI 搜索框 | 网络请求 | |
|---|---|---|
| Publisher 来源 | searchBar.textDidChangePublisher | URLSession.dataTaskPublisher |
| Output 类型 | String | (Data, URLResponse) |
| Failure 类型 | Never(不会出错) | URLError |
| 会发出几次 output | 无数次(用户不断输入) | 最多一次(请求返回) |
| 会 finished 吗 | 通常不会 | 成功后会 |
如何自己创建一个 Publisher:PassthroughSubject
上面两个例子都是用系统提供的现成 Publisher。如果你需要手动控制何时发出值,可以用 PassthroughSubject:
// 创建一个 Publisher,Output 是 String,Failure 是 Never
let subject = PassthroughSubject<String, Never>()
// 手动发出一个新值(output 事件)
subject.send("Hello")
subject.send("World")
// 手动发出完成(finished 事件)
subject.send(completion: .finished)
// 手动发出错误(failure 事件)
// subject.send(completion: .failure(someError))
这在写单元测试、或者需要把旧代码的回调桥接成 Publisher 时非常有用。
@Published 和 Publisher 的关系
@Published 是一个属性包装器,它本质上是自动帮你创建了一个 Publisher 的语法糖。
class UserViewModel: ObservableObject {
@Published var name: String = ""
}
当你声明 @Published var name 时,Swift 自动生成了一个 $name,它就是一个 Publisher<String, Never>。每当 name 的值发生变化,$name 就自动发出新值。
name = "Alice" → $name 发出 "Alice"
name = "Bob" → $name 发出 "Bob"
Publisher是协议,定义了"能发布事件"这件事的规范@Published是快捷方式,帮你自动实现了一个符合 Publisher 协议的发布者PassthroughSubject是手动版,你自己决定什么时候 send
与传统回调的对比
两者各有适用场景,没有绝对的优劣:
| 传统回调 / Delegate | Combine Publisher | |
|---|---|---|
| 学习曲线 | 低,直观易懂 | 高,概念较多 |
| 适合场景 | 简单的一对一事件响应 | 多个异步流需要组合、变换 |
| 苹果框架集成 | 原生支持,UIKit/AppKit 无缝使用 | 部分旧 API 需要桥接 |
| 多流组合 | 手动处理,代码量大 | 操作符链式处理,优势明显 |
| 可读性 | 简单场景下更易读 | 复杂场景下更易读 |
| 取消订阅 | 手动管理,成熟稳定 | Cancellable 统一管理 |
💡 Delegate 是苹果生态中成熟且够用的模式。Combine 的真正优势在于多个异步操作需要组合的复杂场景,而不是全面替代 Delegate。