Combine 的真正威力不仅在于它自身的操作符体系,更在于它与 Apple 原生框架的深度融合。从通知中心到定时器,从 KVO 到网络请求,Combine 为这些系统 API 提供了一致的响应式接口。掌握这些集成方式,能让你在处理系统级事件时,告别散落的闭包和委托,享受声明式数据流带来的简洁与统一。
1. 系统集成全景图
Combine 原生提供了以下几种系统框架的 Publisher 扩展:
| 框架 | Publisher 获取方式 | 典型场景 |
|---|---|---|
| NotificationCenter | .publisher(for:object:) |
监听系统通知、自定义事件 |
| Timer | Timer.publish(every:on:in:) |
定时任务、倒计时、轮询 |
| KVO | publisher(for:options:) |
观察对象属性变化 |
| URLSession | .dataTaskPublisher(for:) |
网络请求、文件上传下载 |
这些 Publisher 将原本需要闭包、委托或 target-action 的异步事件,统一包装成了可组合、可取消的数据流。
2. NotificationCenter:让通知流动起来
通知是 iOS 中最松散的通信方式,一个发送方、零或多个接收方。Combine 为 NotificationCenter 添加了 .publisher(for:) 方法,将任意通知转化为 Publisher。
2.1 基础用法
swift
import Combine
let cancellable = NotificationCenter.default
.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { notification in
print("应用进入前台")
}
关键点 :返回的 Publisher 不会自动完成(Failure 为 Never),它会持续发出匹配的通知,直到你取消订阅。因此,必须将返回的 AnyCancellable 存入 Set<AnyCancellable> 中。
2.2 提取 userInfo 中的数据
swift
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
}
NotificationCenter.default.publisher(for: .userDidLogin)
.compactMap { $0.userInfo?["username"] as? String }
.sink { username in
print("用户登录: \(username)")
}
.store(in: &cancellables)
使用 compactMap 安全地提取可选值,过滤掉不符合类型的数据,避免在 sink 中做强制解包。
2.3 使用自定义通知解耦模块
通知在模块间解耦时非常有用。例如,在认证模块发出登录通知,其他模块各自响应:
swift
// 发送方
NotificationCenter.default.post(
name: .userDidLogin,
object: nil,
userInfo: ["username": "张三"]
)
// 接收方 A - 更新用户界面
NotificationCenter.default.publisher(for: .userDidLogin)
.receive(on: DispatchQueue.main)
.sink { _ in updateUI() }
.store(in: &cancellables)
// 接收方 B - 刷新缓存
NotificationCenter.default.publisher(for: .userDidLogin)
.sink { _ in refreshCache() }
.store(in: &cancellables)
3. Timer:定时器的新写法
传统 Timer 需要通过 scheduledTimer 或 RunLoop 添加,并在不需要时手动 invalidate。Combine 将其包装为 Timer.publish,以 Publisher 的形式发送 Date 值。
3.1 参数说明
swift
Timer.publish(every: 1.0, on: .main, in: .common)
every:时间间隔(秒)on:Timer 所依附的RunLoop,主线程用.mainin:RunLoop 的模式,.common覆盖.default和.eventTracking
3.2 基础定时器
swift
let timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { date in
print("当前时间: \(date)")
}
为什么需要 autoconnect()?
Timer.publish 返回的是一个 ConnectablePublisher,它不会在订阅时自动启动。.autoconnect() 让它在第一个订阅者到达时自动开始。如果忘记调用,Timer 将永远不会触发。
3.3 倒计时示例
swift
class CountdownViewModel: ObservableObject {
@Published var remaining = 60
@Published var isRunning = false
private var bag = Set<AnyCancellable>()
func start() {
isRunning = true
remaining = 60
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.prefix(while: { [weak self] _ in self?.remaining ?? 0 > 0 })
.sink { [weak self] _ in
self?.remaining -= 1
} receiveCompletion: { [weak self] _ in
self?.isRunning = false
}
.store(in: &bag)
}
func stop() {
bag.removeAll()
isRunning = false
}
}
这里使用 prefix(while:) 自动在倒计时归零时完成并停止定时器,无需手动计算次数。
3.4 控制启停
如果需要手动控制定时器的启停,使用 makeConnectable() 并保存 connect() 返回的 AnyCancellable:
swift
let timer = Timer.publish(every: 1, on: .main, in: .common)
.makeConnectable()
let connection = timer.connect()
// 暂停
connection.cancel()
// 恢复
let newConnection = timer.connect()
4. KVO:观察 Objective-C 属性
Combine 通过 publisher(for:options:) 方法为任何遵循 NSObject 且属性标记为 @objc dynamic 的对象提供 KVO Publisher。
4.1 基本用法
swift
class LegacyObject: NSObject {
@objc dynamic var value: Int = 0
}
let obj = LegacyObject()
obj.publisher(for: \.value, options: [.new])
.sink { newValue in print("值变为: \(newValue)") }
.store(in: &bag)
obj.value = 42 // 输出: 值变为: 42
4.2 KVO 在 SwiftUI 中的局限
KVO 要求被观察对象继承自 NSObject,且属性为 @objc dynamic,这违背了 Swift 的类型安全理念。在 SwiftUI 中,应优先使用 @Published ,只有观察非自有对象(如 OperationQueue.operationCount)时才使用 KVO。
swift
class ViewModel: ObservableObject {
@Published var value = 0 // ✅ 推荐
}
class OldViewModel: ObservableObject {
@objc dynamic var value: Int = 0 // ❌ 不推荐,除非必须兼容 ObjC
}
4.3 实际用途
KVO Publisher 的主要价值在于观察系统类或第三方库中已有的 dynamic 属性,例如 URLSession 的任务状态、AVPlayer 的播放进度等,而非在业务代码中新建 KVO 属性。
5. URLSession:声明式网络层
URLSession.dataTaskPublisher(for:) 可能是 Combine 中使用最频繁的系统集成点。它将传统的 completion handler 网络请求转化为 Publisher,与操作符链无缝衔接。
5.1 基础请求
swift
struct User: Codable {
let id: Int
let name: String
}
func fetchUser(id: Int) -> AnyPublisher<User, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data) // 提取 Data
.decode(type: User.self, decoder: JSONDecoder()) // 解码
.receive(on: DispatchQueue.main) // 切回主线程
.eraseToAnyPublisher()
}
5.2 带错误处理与重试的完整请求
swift
class UserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage: String?
private var bag = Set<AnyCancellable>()
func loadUser(id: Int) {
isLoading = true
errorMessage = nil
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
return data
}
.decode(type: User.self, decoder: JSONDecoder())
.retry(2) // 失败自动重试 2 次
.catch { error -> AnyPublisher<User, Never> in
Just(User.placeholder)
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.sink { [weak self] user in
self?.isLoading = false
self?.user = user
}
.store(in: &bag)
}
}
5.3 并行请求
使用 Publishers.Zip 或 MergeMany 可以组合多个并发请求:
swift
let userPublisher = fetchUser(id: 1)
let postsPublisher = fetchPosts(for: 1)
Publishers.Zip(userPublisher, postsPublisher)
.sink { user, posts in
print("用户: \(user.name), 文章数: \(posts.count)")
}
.store(in: &bag)
6. 综合实战:自动刷新仪表盘
结合 Timer 和 URLSession,创建一个每 30 秒自动刷新数据的仪表盘:
swift
class DashboardViewModel: ObservableObject {
@Published var stats: [Stat] = []
@Published var lastUpdated: Date?
@Published var isLoading = false
private var bag = Set<AnyCancellable>()
func startAutoRefresh() {
refresh() // 立即加载
Timer.publish(every: 30, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in self?.refresh() }
.store(in: &bag)
}
private func refresh() {
isLoading = true
URLSession.shared.dataTaskPublisher(for: statsURL)
.map(\.data)
.decode(type: [Stat].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
self?.isLoading = false
} receiveValue: { [weak self] stats in
self?.stats = stats
self?.lastUpdated = Date()
}
.store(in: &bag)
}
}
7. 最佳实践
7.1 生命周期管理
- 所有系统 Publisher(尤其是 Timer 和 NotificationCenter)返回的订阅必须存入
Set<AnyCancellable>。 - 在视图消失时(
onDisappear)或 ViewModel 析构时(deinit),cancellables.removeAll()会自动取消所有订阅,防止内存泄漏或定时器继续运行。
7.2 线程安全
NotificationCenter的 Publisher 在发出通知的线程 上触发,因此更新 UI 前必须添加.receive(on: DispatchQueue.main)。Timer的线程由on:参数指定,通常设为.main。
7.3 错误处理
URLSession.dataTaskPublisher可能因网络问题失败。使用retry应对瞬时故障,使用catch提供降级数据或默认值,避免整个流终止。- 对于不产生错误的 Publisher(如
NotificationCenter、Timer),其Failure为Never,无需错误处理。
7.4 选择合适的工具
- 简单的定时任务用
Timer.publish。 - 跨模块通信用
NotificationCenter。 - 网络层必须用
URLSession.dataTaskPublisher。 - KVO 仅用于观察系统或第三方框架的对象,业务代码用
@Published。
7.5 调试
- 在链中插入
.print("标签")来观察系统 Publisher 的事件流。 - 使用
.handleEvents监控网络请求的各个阶段,便于定位问题。
8. 总结
Combine 与系统框架的集成是其最具实用价值的部分之一。NotificationCenter.publisher 将松散的通知转化为可组合的流,Timer.publish 让定时器融入响应式链,URLSession.dataTaskPublisher 使网络层声明式化。这些系统 Publisher 拥有统一的订阅、取消和错误处理机制,让你可以用同一种范式应对所有异步场景。
掌握了这些集成技术,你就能在日常开发中逐步用 Combine 替换散乱的回调、委托和 target-action,让代码更加简洁、统一且易于维护。