Combine 与系统框架集成:将响应式编程融入 Apple 生态

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 需要通过 scheduledTimerRunLoop 添加,并在不需要时手动 invalidate。Combine 将其包装为 Timer.publish,以 Publisher 的形式发送 Date 值。

3.1 参数说明

swift 复制代码
Timer.publish(every: 1.0, on: .main, in: .common)
  • every:时间间隔(秒)
  • on:Timer 所依附的 RunLoop,主线程用 .main
  • in: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.ZipMergeMany 可以组合多个并发请求:

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(如 NotificationCenterTimer),其 FailureNever,无需错误处理。

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,让代码更加简洁、统一且易于维护。

相关推荐
90后的晨仔2 小时前
Combine 与 Swift Concurrency:响应式与并发的完美协奏
ios
90后的晨仔2 小时前
Combine 自定义 Subject:构建专属的响应式事件源
ios
90后的晨仔2 小时前
Combine 架构模式:构建响应式应用的蓝图
ios
90后的晨仔2 小时前
Combine 高级实践:多线程调度、调试与测试
ios
人月神话Lee4 小时前
【图像处理】饱和度——颜色的浓淡与灰度化
ios·ai编程·图像识别
王飞飞不会飞5 小时前
iOS卡顿查找和定位-ProFile
ios·性能优化
敲代码的鱼5 小时前
NFC读卡能力 支持安卓/iOS/鸿蒙 UTS插件
android·ios·uni-app
sweet丶9 小时前
iOS应用启动过程深度分析与优化实践
ios
largecode12 小时前
企业名称能在来电显示吗?号码显示公司名服务打通多终端展示
android·xml·ios·iphone·xcode·webview·phonegap