01-研究优秀开源框架@响应式编程@iOS | Combine框架:使用介绍

8 阅读22分钟

📋 目录


一、Combine框架使用详解

1. Combine框架概述

Combine 是 Apple 在 WWDC 2019 推出的响应式编程框架,用于处理异步事件流。它基于 ReactiveX 的设计思想,提供了声明式的 API 来处理时间序列数据。

1.1 什么是Combine

Combine 是一个声明式的 Swift 框架,用于处理随时间变化的值。它允许你通过组合(combine)不同的操作符来创建复杂的数据处理管道。

核心特点:

  • 声明式编程:描述"做什么"而不是"怎么做"
  • 函数式编程:使用高阶函数和操作符组合
  • 类型安全:充分利用 Swift 的类型系统
  • 异步处理:优雅地处理异步操作
  • 错误处理:统一的错误处理机制

1.2 Combine vs 其他框架

特性CombineRxSwiftReactiveSwift
平台Apple 生态(iOS 13+)跨平台跨平台
语言SwiftSwiftSwift
官方支持✅ Apple 官方❌ 第三方❌ 第三方
性能高度优化良好良好
学习曲线中等陡峭陡峭
与系统集成深度集成需要适配需要适配

1.3 适用场景

  • 网络请求:处理 API 响应
  • 用户输入:处理文本输入、按钮点击
  • 数据绑定:UI 与数据模型的双向绑定
  • 状态管理:管理应用状态变化
  • 事件处理:处理通知、定时器等事件

1.4 编程思想(背后的范式与理念)

Combine 的 API 和设计深受几种编程思想影响,理解这些思想能更快抓住「为什么这样写」而不是「怎么背 API」。

(1)响应式编程(Reactive Programming)

  • 核心:把「数据与事件」抽象成随时间推进的流,通过订阅对流中的每个值做出反应,而不是轮询或回调嵌套。
  • 在 Combine 中Publisher 就是一条流,Subscriber 订阅后对每个 receive(_ input:) 做出反应;用户输入、网络结果、定时器都可以统一成同一种「流」,用同一套操作符处理。
  • 与命令式的对比:命令式是「先做 A,再做 B,再根据结果做 C」;响应式是「当流里出现满足某条件的数据时,做 C」,逻辑由数据驱动。

(2)声明式 vs 命令式

维度命令式(Imperative)声明式(Declarative)
关注点「怎么做」:一步步写清执行顺序与分支「做什么」:描述期望的结果与约束
典型写法循环、if-else、回调里再调回调链式操作符:map / filter / combineLatest
在 Combine 中手写「请求 → 等回调 → 解析 → 再请求」publisher.map(...).flatMap(...).sink(...) 描述数据如何变换与消费

声明式让「数据流」一目了然,可读性和可测试性更好;Combine 的链式调用就是声明式的一种体现。

(3)函数式思想(Composition & Immutability)

  • 组合(Composition):小能力组合成大能力。每个操作符只做一件事(map 只做变换、filter 只做过滤),通过 .map(...).filter(...) 组合成完整管道,而不是写一个巨大的闭包。
  • 不可变(Immutability):操作符不修改上游 Publisher,而是返回新的 Publisher;上游保持不变,便于推理和复用。
  • 纯函数倾向:变换用无副作用的闭包(给定相同输入得到相同输出),副作用集中在 sinkassign 等「终端」处,便于测试和并发。

(4)流与时间序列(Streams & Time)

  • 把一切可观测的「变化」都看成时间上的序列:第 1 个值、第 2 个值、……、完成或错误。
  • 操作符可以针对「时间」语义:debounce(等一段时间再发)、throttle(间隔内只发一次)、delay(延后发射),从而统一处理「何时」而不只是「何值」。

(5)观察者与发布-订阅(Observer & Pub-Sub)

  • 观察者模式:观察者订阅被观察对象,在状态变化时得到通知。Combine 里 Subscriber 观察 Publisher。
  • 发布-订阅:发布者与订阅者解耦,通过「订阅」建立连接;Combine 用 Subscription 表示这次连接,用 request(Demand) 控制拉取节奏,是带背压的发布-订阅。

把以上几点串起来:Combine 用声明式(Publisher)和组合式操作符,在发布-订阅模型下做响应式的数据与事件处理,并借 Scheduler 控制时间与线程。理解这些思想后,再看到「为什么用 map 而不是在 sink 里写一坨」「为什么要 subscribe(on:) / receive(on:)」就会更自然。

1.5 原理概览(为何这样设计)

Combine 的核心理念可以概括为以下几点,便于后续理解「架构」与「信息流」:

理念说明
发布-订阅Publisher 不主动推数据,只有 Subscriber 通过 Subscription.request(demand) 请求后,才按需发送;这样下游可以控制节奏,避免被上游淹没。
背压(Backpressure)Subscriber.receive(_ input:) 的返回值类型是 Subscribers.Demand,表示「还能再要多少」;上游根据 Demand 决定是否继续发送,实现流控。
链式不可变每个操作符(map、filter 等)都返回新的 Publisher,不修改原 Publisher;整条链是值类型组合,易于推理和测试。
调度与线程谁在哪个线程执行由 Scheduler 决定;subscribe(on:) 指定「上游与订阅建立」所在线程,receive(on:) 指定「下游收值」所在线程,便于 UI 与后台分离。

后续「二、源码解析」中的内部架构、响应者链、信息流会与上述四点一一对应。


2. 核心概念

2.1 Publisher(发布者)

Publisher 是 Combine 的核心协议,表示可以发布值的类型。

protocol Publisher {
    associatedtype Output
    associatedtype Failure: Error
    
    func receive<S>(subscriber: S) where S: Subscriber, 
        S.Input == Output, S.Failure == Failure
}

特点:

  • 可以发布零个或多个值
  • 可能以完成或错误结束
  • 是值类型(struct)
  • 不可变(每次操作返回新的 Publisher)

示例:

// 创建一个简单的 Publisher:Just 发布单个值后立即完成
let publisher = Just("Hello, Combine!")
    .sink { value in
        print(value)  // 输出: Hello, Combine!
    }

// 使用 Sequence 的 publisher 扩展,将数组转为发布者,按序发布每个元素
let arrayPublisher = [1, 2, 3, 4, 5].publisher
    .sink { value in
        print(value)  // 依次输出: 1, 2, 3, 4, 5
    }

2.2 Subscriber(订阅者)

Subscriber 是接收 Publisher 发布值的协议。

protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error
    
    func receive(subscription: Subscription)
    func receive(_ input: Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Failure>)
}

内置 Subscriber:

  • sink:最简单的订阅方式
  • assign:将值赋给对象的属性

示例:

// 使用 sink 订阅:同时处理「值」与「完成/错误」
let cancellable = [1, 2, 3].publisher
    .sink(
        receiveCompletion: { completion in
            // 流结束时的回调:.finished 或 .failure(error)
            switch completion {
            case .finished:
                print("完成")
            case .failure(let error):
                print("错误: \(error)")
            }
        },
        receiveValue: { value in
            print("收到值: \(value)")
        }
    )

// 使用 assign 订阅:将每个发布的值赋给对象的某个属性(KeyPath)
class ViewModel {
    @Published var count: Int = 0
}

let viewModel = ViewModel()
let cancellable = [1, 2, 3].publisher
    .assign(to: \.count, on: viewModel)  // 最终 viewModel.count == 3

2.3 Subscription(订阅)

Subscription 表示订阅关系,控制数据流的生命周期。

protocol Subscription: Cancellable, CustomCombineIdentifierConvertible {
    func request(_ demand: Subscribers.Demand)
}

关键点:

  • 控制数据流的开始和结束
  • 实现背压(backpressure)控制
  • 可以取消订阅

示例:

// 自定义 Subscriber,演示背压:通过 request(.max(3)) 只拉取 3 个值
class CustomSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never
    
    func receive(subscription: Subscription) {
        // 建立订阅后,主动请求最多 3 个值(背压控制)
        subscription.request(.max(3))
    }
    
    func receive(_ input: Int) -> Subscribers.Demand {
        print("收到: \(input)")
        // 返回 .none 表示本轮不再请求更多;上游最多只会发 3 个
        return .none
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        print("完成")
    }
}

let subscriber = CustomSubscriber()
// 数组有 5 个元素,但只会收到 1、2、3
[1, 2, 3, 4, 5].publisher.subscribe(subscriber)

3. Publisher与Subscriber

3.1 内置Publisher类型

Just

发布单个值然后完成。

// Just:有订阅时发布一个值并立即发送 .finished
let just = Just("Hello")
    .sink { value in
        print(value)  // 输出: Hello
    }
Future

异步执行操作并发布结果。

// Future:封装异步回调,只执行一次,结果通过 promise 发布
func fetchData() -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            promise(.success("数据加载完成"))
        }
    }
}

fetchData()
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("错误: \(error)")
            }
        },
        receiveValue: { value in
            print(value)  // 输出: 数据加载完成
        }
    )
Deferred

延迟创建 Publisher,直到有订阅者。

// Deferred:闭包在「第一次被订阅」时才执行,避免创建时就产生副作用
let deferred = Deferred {
    Future { promise in
        print("开始执行")
        promise(.success("结果"))
    }
}

// 此时不会执行(只创建了 Deferred,未订阅)
print("创建完成")

// 订阅时才执行内部 Future,并收到 "结果"
deferred.sink { value in
    print(value)  // 输出: 开始执行, 结果
}
Empty

不发布任何值,可选择立即完成或永不完成。Empty 是 Combine 中非常有用的占位符 Publisher,常用于条件分支、错误处理、以及保持订阅活跃。

基本用法:

// 立即完成:不发送任何 value,只发送 completion
let empty = Empty<String, Never>(completeImmediately: true)
    .sink(
        receiveCompletion: { _ in print("完成") },
        receiveValue: { _ in }
    )

// 永不完成:既不发值也不发 completion,常用于测试或「占位」
let never = Empty<String, Never>(completeImmediately: false)

Empty 的占位操作:

Empty 最常见的用途是作为占位符 Publisher,在条件不满足时提供一个"空"的 Publisher,避免返回 Optional 或处理 nil 的情况。

1. 条件分支中的占位

// 场景:根据条件返回不同的 Publisher
func fetchData(shouldFetch: Bool) -> AnyPublisher<String, Never> {
    if shouldFetch {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .compactMap { String(data: $0, encoding: .utf8) }
            .replaceError(with: "")
            .eraseToAnyPublisher()
    } else {
        // 使用 Empty 作为占位,不执行任何操作
        return Empty(completeImmediately: true)
            .eraseToAnyPublisher()
    }
}

// 使用:无论条件如何,返回类型都是 AnyPublisher<String, Never>
fetchData(shouldFetch: true)
    .sink { print($0) }  // 正常接收数据

fetchData(shouldFetch: false)
    .sink { print($0) }  // 立即完成,不接收任何值

2. 错误处理中的占位

// 在 catch 中使用 Empty 作为备用 Publisher
func loadUserData() -> AnyPublisher<User, Error> {
    return URLSession.shared.dataTaskPublisher(for: userURL)
        .map(\.data)
        .decode(type: User.self, decoder: JSONDecoder())
        .catch { error -> AnyPublisher<User, Error> in
            if error is DecodingError {
                // 解码错误时返回空 Publisher,不发送任何值
                return Empty(completeImmediately: true)
                    .eraseToAnyPublisher()
            } else {
                // 其他错误继续传播
                return Fail(error: error)
                    .eraseToAnyPublisher()
            }
        }
        .eraseToAnyPublisher()
}

3. flatMap 中的条件占位

// 在 flatMap 中根据条件决定是否执行操作
class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var results: [String] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $searchText
            .flatMap { query -> AnyPublisher<[String], Never> in
                if query.isEmpty {
                    // 空查询时返回 Empty,不执行搜索
                    return Empty(completeImmediately: true)
                        .eraseToAnyPublisher()
                } else {
                    // 执行搜索
                    return self.search(query: query)
                        .catch { _ in Just([]) }
                        .eraseToAnyPublisher()
                }
            }
            .assign(to: \.results, on: self)
            .store(in: &cancellables)
    }
    
    private func search(query: String) -> AnyPublisher<[String], Error> {
        // 搜索实现
        return Just(["结果1", "结果2"])
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

4. 使用 Empty 保持订阅活跃(常驻任务)

Empty 的 completeImmediately: false 模式可以创建一个永不完成的 Publisher,这在需要保持订阅活跃、贯穿整个程序生命周期的场景中非常有用。

场景1:常驻的后台任务

class BackgroundTaskManager {
    private var cancellables = Set<AnyCancellable>()
    
    // 创建一个永不完成的 Empty 作为基础流
    private let keepAlive = Empty<Never, Never>(completeImmediately: false)
        .eraseToAnyPublisher()
    
    func startBackgroundTask() {
        // 使用 flatMap 将 Empty 转换为周期性的任务流
        keepAlive
            .flatMap { _ -> AnyPublisher<Date, Never> in
                // 每 5 秒执行一次任务
                return Timer.publish(every: 5.0, on: .main, in: .common)
                    .autoconnect()
                    .map { _ in Date() }
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] date in
                self?.performBackgroundTask(at: date)
            }
            .store(in: &cancellables)
    }
    
    private func performBackgroundTask(at date: Date) {
        print("执行后台任务: \(date)")
        // 执行实际的后台任务,如数据同步、状态检查等
    }
    
    func stopBackgroundTask() {
        cancellables.removeAll()  // 取消所有订阅
    }
}

场景2:常驻的事件监听

class AppLifecycleManager {
    private var cancellables = Set<AnyCancellable>()
    
    func startMonitoring() {
        // 使用 Empty 作为基础流,保持订阅活跃
        Empty<Never, Never>(completeImmediately: false)
            .flatMap { _ -> AnyPublisher<Notification, Never> in
                // 监听多个通知
                let appWillEnterForeground = NotificationCenter.default
                    .publisher(for: UIApplication.willEnterForegroundNotification)
                
                let appDidEnterBackground = NotificationCenter.default
                    .publisher(for: UIApplication.didEnterBackgroundNotification)
                
                // 合并多个通知流
                return Publishers.Merge(appWillEnterForeground, appDidEnterBackground)
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] notification in
                self?.handleAppLifecycleEvent(notification)
            }
            .store(in: &cancellables)
    }
    
    private func handleAppLifecycleEvent(_ notification: Notification) {
        switch notification.name {
        case UIApplication.willEnterForegroundNotification:
            print("应用即将进入前台")
        case UIApplication.didEnterBackgroundNotification:
            print("应用进入后台")
        default:
            break
        }
    }
}

场景3:常驻的心跳/保活机制

class HeartbeatManager {
    private var cancellables = Set<AnyCancellable>()
    private let heartbeatInterval: TimeInterval = 30.0
    
    func startHeartbeat() {
        // 使用 Empty 保持订阅,然后转换为心跳流
        Empty<Never, Never>(completeImmediately: false)
            .flatMap { [weak self] _ -> AnyPublisher<Void, Never> in
                guard let self = self else {
                    return Empty(completeImmediately: true).eraseToAnyPublisher()
                }
                
                // 创建心跳定时器
                return Timer.publish(every: self.heartbeatInterval, on: .main, in: .common)
                    .autoconnect()
                    .map { _ in () }
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] _ in
                self?.sendHeartbeat()
            }
            .store(in: &cancellables)
    }
    
    private func sendHeartbeat() {
        // 发送心跳请求
        print("发送心跳: \(Date())")
        // 实际实现:发送网络请求保持连接活跃
    }
    
    func stopHeartbeat() {
        cancellables.removeAll()
    }
}

场景4:常驻的数据同步任务

class DataSyncManager {
    private var cancellables = Set<AnyCancellable>()
    private let syncInterval: TimeInterval = 60.0
    
    func startAutoSync() {
        // 使用 Empty 作为基础流,保持订阅贯穿应用生命周期
        Empty<Never, Never>(completeImmediately: false)
            .flatMap { [weak self] _ -> AnyPublisher<SyncResult, Never> in
                guard let self = self else {
                    return Empty(completeImmediately: true).eraseToAnyPublisher()
                }
                
                // 创建周期性同步流
                return Timer.publish(every: self.syncInterval, on: .main, in: .common)
                    .autoconnect()
                    .flatMap { _ -> AnyPublisher<SyncResult, Never> in
                        return self.performSync()
                            .catch { _ in Just(SyncResult.failure) }
                            .eraseToAnyPublisher()
                    }
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] result in
                self?.handleSyncResult(result)
            }
            .store(in: &cancellables)
    }
    
    private func performSync() -> AnyPublisher<SyncResult, Error> {
        // 执行数据同步
        return Just(SyncResult.success)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
    
    private func handleSyncResult(_ result: SyncResult) {
        print("同步结果: \(result)")
    }
    
    enum SyncResult {
        case success
        case failure
    }
}

场景5:更优雅的常驻任务实现(推荐方式)

虽然 Empty 可以用来保持订阅,但更推荐使用 Timer.publish().autoconnect()PassthroughSubject 来实现常驻任务:

class BetterBackgroundTaskManager {
    private var cancellables = Set<AnyCancellable>()
    
    func startBackgroundTask() {
        // 方式1:直接使用 Timer(推荐)
        Timer.publish(every: 5.0, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                self?.performTask()
            }
            .store(in: &cancellables)
        
        // 方式2:使用 PassthroughSubject 控制(更灵活)
        let taskTrigger = PassthroughSubject<Void, Never>()
        
        taskTrigger
            .sink { [weak self] _ in
                self?.performTask()
            }
            .store(in: &cancellables)
        
        // 可以手动触发或结合 Timer
        Timer.publish(every: 5.0, on: .main, in: .common)
            .autoconnect()
            .sink { _ in taskTrigger.send() }
            .store(in: &cancellables)
    }
    
    private func performTask() {
        print("执行任务")
    }
}

Empty 占位操作的最佳实践:

  1. 类型一致性:使用 Empty 时确保类型匹配(Output 和 Failure)
  2. 立即完成 vs 永不完成
    • completeImmediately: true:用于条件分支,表示"跳过此分支"
    • completeImmediately: false:用于保持订阅活跃,但更推荐使用 Timer 或 Subject
  3. 结合 eraseToAnyPublisher():在使用 Empty 时通常需要类型擦除,以保持类型一致性
  4. 避免过度使用:对于常驻任务,优先考虑 Timer 或 PassthroughSubject,Empty 更适合作为占位符

Empty 的常见使用模式总结:

使用场景completeImmediately说明
条件分支占位true条件不满足时返回空流
错误处理占位true某些错误情况下不发送值
测试占位false测试中模拟永不完成的流
保持订阅(不推荐)false可用但更推荐 Timer/Subject

注意事项:

  • Empty 是值类型(struct),每次创建都是新实例
  • completeImmediately: false 的 Empty 会保持订阅活跃,但不会发送任何值
  • 对于常驻任务,虽然可以用 Empty 实现,但使用 Timer 或 PassthroughSubject 更直观和高效
Fail

立即发布错误。

// Fail:有订阅时立即发送 .failure(error),不发送任何正常值
enum MyError: Error {
    case customError
}

let fail = Fail<String, MyError>(error: .customError)
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("错误: \(error)")
            }
        },
        receiveValue: { _ in }
    )
Sequence

从序列创建 Publisher。

// 符合 Sequence 的类型都有 .publisher,按顺序发布元素
let sequence = (1...5).publisher
    .sink { value in
        print(value)  // 输出: 1, 2, 3, 4, 5
    }

3.2 自定义Publisher

// 自定义 Publisher:从数组按需发布元素,遵循背压
struct CustomPublisher: Publisher {
    typealias Output = Int
    typealias Failure = Never
    
    let values: [Int]
    
    func receive<S>(subscriber: S) where S: Subscriber, 
        S.Input == Output, S.Failure == Failure {
        // 收到订阅者时,创建自定义 Subscription 并下发给订阅者
        let subscription = CustomSubscription(
            subscriber: subscriber,
            values: values
        )
        subscriber.receive(subscription: subscription)
    }
}

// 自定义 Subscription:根据 request(demand) 按需从 values 取数并下发
class CustomSubscription<S: Subscriber>: Subscription 
    where S.Input == Int, S.Failure == Never {
    
    var subscriber: S?
    let values: [Int]
    var currentIndex = 0
    var requested: Subscribers.Demand = .none
    
    init(subscriber: S, values: [Int]) {
        self.subscriber = subscriber
        self.values = values
    }
    
    func request(_ demand: Subscribers.Demand) {
        requested += demand
        
        // 在 demand 允许且还有数据时,逐个下发
        while requested > .none && currentIndex < values.count {
            let value = values[currentIndex]
            currentIndex += 1
            requested -= .max(1)
            
            _ = subscriber?.receive(value)
        }
        
        if currentIndex >= values.count {
            subscriber?.receive(completion: .finished)
            cancel()
        }
    }
    
    func cancel() {
        subscriber = nil
    }
}

// 使用自定义 Publisher:行为等价于 [1,2,3].publisher
let custom = CustomPublisher(values: [1, 2, 3])
    .sink { value in
        print(value)  // 输出: 1, 2, 3
    }

4. Operators操作符

4.1 转换操作符

map

转换每个值。

// map:对每个元素做变换,类型可改变
[1, 2, 3].publisher
    .map { $0 * 2 }
    .sink { print($0) }  // 输出: 2, 4, 6
flatMap

将多个 Publisher 扁平化。

// flatMap:每个元素映射为一个新 Publisher,再把这些 Publisher 的输出「压平」成一条流
["A", "B", "C"].publisher
    .flatMap { letter in
        (1...2).publisher.map { "\(letter)\($0)" }
    }
    .sink { print($0) }  // 输出: A1, A2, B1, B2, C1, C2
compactMap

过滤 nil 值。

// compactMap:类似 map,但闭包返回 Optional;nil 会被丢弃,不往下游发
["1", "2", "abc", "3"].publisher
    .compactMap { Int($0) }
    .sink { print($0) }  // 输出: 1, 2, 3
scan

累积值。

// scan:给定初始值,每收到一个元素就与当前累积值做运算,并下发新的累积值
[1, 2, 3, 4, 5].publisher
    .scan(0, +)
    .sink { print($0) }  // 输出: 1, 3, 6, 10, 15

4.2 过滤操作符

filter

过滤值。

// filter:只下发谓词为 true 的值
[1, 2, 3, 4, 5].publisher
    .filter { $0 % 2 == 0 }
    .sink { print($0) }  // 输出: 2, 4
removeDuplicates

移除重复值。

// removeDuplicates:连续相同只发第一个,相当于「相邻去重」
[1, 1, 2, 2, 3, 3].publisher
    .removeDuplicates()
    .sink { print($0) }  // 输出: 1, 2, 3
first / last

获取第一个或最后一个值。

// first:只取第一个元素,取到后发完成
[1, 2, 3, 4, 5].publisher
    .first()
    .sink { print($0) }  // 输出: 1

// last:必须等上游完成,再发最后一个元素
[1, 2, 3, 4, 5].publisher
    .last()
    .sink { print($0) }  // 输出: 5
dropFirst / dropLast

丢弃前几个或后几个值。

// dropFirst(n):跳过前 n 个,只发后面的
[1, 2, 3, 4, 5].publisher
    .dropFirst(2)
    .sink { print($0) }  // 输出: 3, 4, 5

4.3 组合操作符

combineLatest

组合多个 Publisher 的最新值。

// combineLatest:两边都至少发过一个值后,每次任一边发新值就组合「两边当前最新值」下发
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .combineLatest(publisher2)
    .sink { value1, value2 in
        print("\(value1): \(value2)")
    }

publisher1.send("A")  // 无输出(publisher2 尚未发过值)
publisher2.send(1)    // 输出: A: 1
publisher1.send("B")  // 输出: B: 1(用 B 与 2 的最新值 1 组合)
publisher2.send(2)    // 输出: B: 2
merge

合并多个 Publisher。

// merge:多个流合并成一条,哪个先发就先收到哪个,类型必须相同
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .merge(with: publisher2)
    .sink { print($0) }

publisher1.send(1)  // 输出: 1
publisher2.send(2)  // 输出: 2
publisher1.send(3)  // 输出: 3
zip

按顺序组合多个 Publisher。

// zip:按「第 n 个与第 n 个」配对,凑齐一对才下发,顺序严格
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .zip(publisher2)
    .sink { value1, value2 in
        print("\(value1): \(value2)")
    }

publisher1.send("A")  // 等待 publisher2 的第一个值
publisher1.send("B")  // 等待 publisher2 的第二个值
publisher2.send(1)    // 输出: A: 1
publisher2.send(2)    // 输出: B: 2

4.4 时间操作符

debounce

防抖,等待指定时间后发布最新值。

// debounce:在一段时间内没有新值时,才把「最后一次收到的值」发出去(适合搜索框)
let subject = PassthroughSubject<String, Never>()

subject
    .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
    .sink { print($0) }

subject.send("H")     // 不输出(等待 0.5s)
subject.send("He")    // 重置等待
subject.send("Hel")   // 重置等待
subject.send("Hell")  // 重置等待
subject.send("Hello") // 0.5 秒内无新值,输出: Hello
throttle

节流,在指定时间间隔内只发布第一个值。

// throttle:在时间窗口内只取一个值;latest: false 取窗口内第一个,true 取最后一个
let subject = PassthroughSubject<String, Never>()

subject
    .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: false)
    .sink { print($0) }

subject.send("A")  // 立即输出: A,开启 1 秒窗口
subject.send("B")  // 不输出(1 秒内)
subject.send("C")  // 不输出(1 秒内)
// 1 秒后
subject.send("D")  // 输出: D
delay

延迟发布值。

// delay:每个元素都延后指定时间再下发,相对顺序不变
[1, 2, 3].publisher
    .delay(for: .seconds(1), scheduler: DispatchQueue.main)
    .sink { print($0) }  // 1 秒后依次输出: 1, 2, 3

4.5 错误处理操作符

catch

捕获错误并返回备用 Publisher。

// catch:上游失败时用闭包返回一个备用 Publisher,流继续用备用流
enum MyError: Error {
    case failure
}

let publisher = Fail<String, MyError>(error: .failure)
    .catch { error -> Just<String> in
        print("捕获错误: \(error)")
        return Just("备用值")
    }
    .sink { print($0) }  // 输出: 捕获错误: failure, 备用值
retry

重试失败的 Publisher。

// retry(n):失败时重新订阅上游最多 n 次(这里是 2 次,共最多 3 次执行)
var attempts = 0

let publisher = Future<String, Error> { promise in
    attempts += 1
    if attempts < 3 {
        promise(.failure(NSError(domain: "test", code: 1)))
    } else {
        promise(.success("成功"))
    }
}
.retry(2)  // 最多重试 2 次,第 3 次成功
.sink(
    receiveCompletion: { print($0) },
    receiveValue: { print($0) }  // 输出: 成功
)
replaceError

用默认值替换错误。

// replaceError:失败时不发错误,改为发一个默认值并正常结束
let publisher = Fail<String, MyError>(error: .failure)
    .replaceError(with: "默认值")
    .sink { print($0) }  // 输出: 默认值

5. Subjects

Subjects 既是 Publisher 又是 Subscriber,可以手动发送值。

5.1 PassthroughSubject

直接传递值,不保存当前值。

// PassthroughSubject:只转发 send 的值,不存当前值,后订阅的收不到之前的值
let subject = PassthroughSubject<String, Never>()

// 订阅1
let cancellable1 = subject.sink { print("订阅1: \($0)") }

subject.send("A")  // 输出: 订阅1: A

// 订阅2:之后 send 的值两个订阅都会收到
let cancellable2 = subject.sink { print("订阅2: \($0)") }

subject.send("B")  // 输出: 订阅1: B, 订阅2: B

5.2 CurrentValueSubject

保存当前值,新订阅者会立即收到当前值。

// CurrentValueSubject:持有当前 value,新订阅者会先收到当前值再收后续 send
let subject = CurrentValueSubject<String, Never>("初始值")

// 订阅1:立即收到初始值
let cancellable1 = subject.sink { print("订阅1: \($0)") }
// 输出: 订阅1: 初始值

subject.value = "新值"  // 输出: 订阅1: 新值

// 订阅2:一订阅就收到当前值 "新值"
let cancellable2 = subject.sink { print("订阅2: \($0)") }
// 输出: 订阅2: 新值(立即收到当前值)

5.3 @Published 属性包装器

自动创建 Publisher。

// @Published:属性变化时自动发值;$name 是该属性的 Publisher
class ViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    
    init() {
        // 监听 name 的变化,防抖后处理
        $name
            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
            .sink { [weak self] newName in
                print("名称变化: \(newName)")
            }
            .store(in: &cancellables)
    }
    
    private var cancellables = Set<AnyCancellable>()
}

let viewModel = ViewModel()
viewModel.name = "张三"  // 0.5 秒后输出: 名称变化: 张三

5.4 把属性变成 Publisher 的使用案例

在 Combine 中,有多种方式可以将属性转换为 Publisher,每种方式适用于不同的场景。理解这些方式有助于更好地使用 Combine 进行响应式编程。

5.4.1 使用 @Published 属性包装器(推荐)

@Published 是 Combine 中最常用和推荐的方式,特别适合在 ViewModel 或 ObservableObject 中使用。

基本用法:

class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    @Published var isLoggedIn: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 使用 $name 访问 Publisher
        $name
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .sink { [weak self] newName in
                print("名称变化: \(newName)")
                // 可以触发其他操作,如搜索、验证等
            }
            .store(in: &cancellables)
        
        // 监听多个属性
        Publishers.CombineLatest($name, $age)
            .sink { [weak self] name, age in
                print("用户信息: \(name), \(age)")
            }
            .store(in: &cancellables)
    }
}

特点:

  • ✅ 自动创建 Publisher(通过 $属性名 访问)
  • ✅ 类型安全,编译时检查
  • ✅ 与 SwiftUI 深度集成
  • ✅ 自动发送初始值(可通过 dropFirst() 跳过)
5.4.2 使用 CurrentValueSubject

CurrentValueSubject 适合需要手动控制发布时机的场景,或者需要将非 @Published 属性转换为 Publisher。

基本用法:

class SettingsManager {
    // 方式1:直接使用 CurrentValueSubject 作为存储属性
    private let _theme = CurrentValueSubject<String, Never>("light")
    var theme: String {
        get { _theme.value }
        set { _theme.value = newValue }
    }
    
    // 暴露 Publisher
    var themePublisher: AnyPublisher<String, Never> {
        _theme.eraseToAnyPublisher()
    }
    
    // 方式2:将普通属性包装为 CurrentValueSubject
    private var _userName: String = ""
    private let userNameSubject = CurrentValueSubject<String, Never>("")
    
    var userName: String {
        get { _userName }
        set {
            _userName = newValue
            userNameSubject.send(newValue)
        }
    }
    
    var userNamePublisher: AnyPublisher<String, Never> {
        userNameSubject.eraseToAnyPublisher()
    }
}

实际应用场景:

class NetworkManager {
    private let _connectionStatus = CurrentValueSubject<ConnectionStatus, Never>(.disconnected)
    
    var connectionStatus: ConnectionStatus {
        get { _connectionStatus.value }
    }
    
    var connectionStatusPublisher: AnyPublisher<ConnectionStatus, Never> {
        _connectionSubject.eraseToAnyPublisher()
    }
    
    func connect() {
        // 网络连接逻辑
        _connectionStatus.send(.connecting)
        // ... 连接成功后
        _connectionStatus.send(.connected)
    }
    
    enum ConnectionStatus {
        case disconnected
        case connecting
        case connected
    }
}

// 使用
let networkManager = NetworkManager()
networkManager.connectionStatusPublisher
    .sink { status in
        print("连接状态: \(status)")
    }
    .store(in: &cancellables)
5.4.3 使用 PassthroughSubject(不保存当前值)

PassthroughSubject 适合事件类型的属性,不需要保存当前值,只关注变化事件。

基本用法:

class ButtonViewModel {
    // 按钮点击事件
    let buttonTap = PassthroughSubject<Void, Never>()
    
    // 用户操作事件
    let userAction = PassthroughSubject<UserAction, Never>()
    
    enum UserAction {
        case login
        case logout
        case refresh
    }
}

// 使用
let viewModel = ButtonViewModel()
viewModel.buttonTap
    .sink { print("按钮被点击") }
    .store(in: &cancellables)

viewModel.userAction
    .sink { action in
        switch action {
        case .login: print("用户登录")
        case .logout: print("用户登出")
        case .refresh: print("刷新数据")
        }
    }
    .store(in: &cancellables)

// 触发事件
viewModel.buttonTap.send()
viewModel.userAction.send(.login)
5.4.4 使用 KVO(Key-Value Observing)

对于 NSObject 的子类,可以使用 KVO 将属性转换为 Publisher。

基本用法:

import Combine

class Person: NSObject {
    @objc dynamic var name: String = ""
    @objc dynamic var age: Int = 0
}

// 使用
let person = Person()

// 将 KVO 属性转换为 Publisher
person.publisher(for: \.name, options: [.initial, .new])
    .sink { name in
        print("姓名变化: \(name)")
    }
    .store(in: &cancellables)

person.publisher(for: \.age, options: [.initial, .new])
    .sink { age in
        print("年龄变化: \(age)")
    }
    .store(in: &cancellables)

// 修改属性会触发 Publisher
person.name = "张三"  // 输出: 姓名变化: 张三
person.age = 25      // 输出: 年龄变化: 25

KVO Options 说明:

  • .initial:订阅时立即发送当前值
  • .new:属性变化时发送新值
  • .old:属性变化时发送旧值
  • .prior:变化前发送旧值,变化后发送新值
5.4.5 使用 NotificationCenter

将系统通知或自定义通知转换为 Publisher。

基本用法:

// 系统通知
let keyboardWillShow = NotificationCenter.default
    .publisher(for: UIResponder.keyboardWillShowNotification)
    .map { notification -> CGRect in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero
    }

keyboardWillShow
    .sink { frame in
        print("键盘高度: \(frame.height)")
    }
    .store(in: &cancellables)

// 自定义通知
extension Notification.Name {
    static let userDidLogin = Notification.Name("userDidLogin")
    static let dataDidUpdate = Notification.Name("dataDidUpdate")
}

let userLoginPublisher = NotificationCenter.default
    .publisher(for: .userDidLogin)
    .compactMap { $0.userInfo?["user"] as? User }

userLoginPublisher
    .sink { user in
        print("用户登录: \(user.name)")
    }
    .store(in: &cancellables)

// 发送通知
NotificationCenter.default.post(
    name: .userDidLogin,
    object: nil,
    userInfo: ["user": currentUser]
)
5.4.6 使用 Timer 将时间属性转换为 Publisher

将定时器转换为 Publisher,用于周期性更新。

基本用法:

class ClockViewModel {
    // 方式1:使用 Timer.publish
    var currentTime: AnyPublisher<Date, Never> {
        Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .eraseToAnyPublisher()
    }
    
    // 方式2:创建可控制的定时器
    private var timerCancellable: AnyCancellable?
    
    func startTimer() {
        timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] date in
                self?.updateTime(date)
            }
    }
    
    func stopTimer() {
        timerCancellable?.cancel()
        timerCancellable = nil
    }
    
    private func updateTime(_ date: Date) {
        // 更新时间
    }
}
5.4.7 组合多个属性 Publisher

使用 Combine 操作符组合多个属性 Publisher。

场景1:表单验证

class FormViewModel: ObservableObject {
    @Published var username: String = ""
    @Published var password: String = ""
    @Published var confirmPassword: String = ""
    
    // 组合多个属性,实时验证表单
    var isFormValid: AnyPublisher<Bool, Never> {
        Publishers.CombineLatest3($username, $password, $confirmPassword)
            .map { username, password, confirmPassword in
                !username.isEmpty &&
                password.count >= 6 &&
                password == confirmPassword
            }
            .eraseToAnyPublisher()
    }
    
    // 用户名验证
    var usernameValidation: AnyPublisher<String?, Never> {
        $username
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .map { username in
                if username.isEmpty {
                    return "用户名不能为空"
                } else if username.count < 3 {
                    return "用户名至少3个字符"
                }
                return nil
            }
            .eraseToAnyPublisher()
    }
}

场景2:搜索功能

class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var selectedCategory: String = "all"
    @Published var sortOrder: SortOrder = .ascending
    
    enum SortOrder {
        case ascending
        case descending
    }
    
    // 组合多个条件,触发搜索
    var searchTrigger: AnyPublisher<(String, String, SortOrder), Never> {
        Publishers.CombineLatest3($searchText, $selectedCategory, $sortOrder)
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .eraseToAnyPublisher()
    }
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        searchTrigger
            .sink { [weak self] text, category, order in
                self?.performSearch(text: text, category: category, order: order)
            }
            .store(in: &cancellables)
    }
    
    private func performSearch(text: String, category: String, order: SortOrder) {
        // 执行搜索
    }
}

场景3:实时计算属性

class ShoppingCartViewModel: ObservableObject {
    @Published var items: [CartItem] = []
    @Published var discount: Double = 0.0
    @Published var shippingFee: Double = 0.0
    
    // 实时计算总价
    var totalPrice: AnyPublisher<Double, Never> {
        Publishers.CombineLatest3($items, $discount, $shippingFee)
            .map { items, discount, shippingFee in
                let subtotal = items.reduce(0) { $0 + $1.price * Double($1.quantity) }
                let discounted = subtotal * (1 - discount)
                return discounted + shippingFee
            }
            .eraseToAnyPublisher()
    }
    
    // 商品数量变化时自动更新
    var itemCount: AnyPublisher<Int, Never> {
        $items
            .map { $0.reduce(0) { $0 + $1.quantity } }
            .eraseToAnyPublisher()
    }
}

struct CartItem {
    let id: String
    var quantity: Int
    let price: Double
}
5.4.8 属性转换的最佳实践

1. 选择合适的转换方式

场景推荐方式原因
ViewModel/ObservableObject@Published与 SwiftUI 集成,自动管理
需要手动控制发布时机CurrentValueSubject更灵活的控制
事件类型(不保存状态)PassthroughSubject只关注事件,不保存值
NSObject 子类KVO .publisher(for:)利用现有 KVO 机制
系统通知NotificationCenter.publisher系统级事件
定时更新Timer.publish周期性更新

2. 避免内存泄漏

// ✅ 正确:使用 weak self
class ViewModel: ObservableObject {
    @Published var data: String = ""
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $data
            .sink { [weak self] value in
                self?.processData(value)
            }
            .store(in: &cancellables)
    }
    
    private func processData(_ value: String) {
        // 处理数据
    }
}

// ❌ 错误:强引用循环
class ViewModel: ObservableObject {
    @Published var data: String = ""
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $data
            .sink { [self] value in  // 强引用 self
                self.processData(value)
            }
            .store(in: &cancellables)
    }
}

3. 使用 dropFirst() 跳过初始值

class ViewModel: ObservableObject {
    @Published var searchText: String = ""
    
    init() {
        // 跳过初始值,只在用户输入时触发
        $searchText
            .dropFirst()  // 跳过初始的 ""
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .sink { [weak self] text in
                self?.performSearch(text)
            }
            .store(in: &cancellables)
    }
}

4. 类型擦除保持接口简洁

class DataManager {
    private let _data = CurrentValueSubject<[String], Never>([])
    
    // ✅ 暴露类型擦除的 Publisher
    var dataPublisher: AnyPublisher<[String], Never> {
        _data.eraseToAnyPublisher()
    }
    
    // ❌ 不推荐:直接暴露 CurrentValueSubject
    // var dataPublisher: CurrentValueSubject<[String], Never> { _data }
}

5. 组合多个属性的模式

// 模式1:CombineLatest(所有属性都变化时触发)
Publishers.CombineLatest($name, $age)
    .sink { name, age in
        // name 或 age 任一变化都会触发
    }

// 模式2:Zip(需要成对变化)
Publishers.Zip($name, $age)
    .sink { name, age in
        // name 和 age 必须都变化一次才触发
    }

// 模式3:Merge(任一变化时触发)
Publishers.Merge($name.map { "name: \($0)" }, $age.map { "age: \($0)" })
    .sink { message in
        // name 或 age 变化都会触发
    }

6. Schedulers调度器

Schedulers 决定操作在哪个线程执行。

6.1 内置Scheduler

DispatchQueue
// subscribe(on:):订阅与上游工作在哪个调度器;receive(on:):下游收值在哪个调度器(常用主线程更新 UI)
[1, 2, 3].publisher
    .subscribe(on: DispatchQueue.global())  // 在后台线程执行订阅与上游
    .receive(on: DispatchQueue.main)        // 在主线程接收并执行 sink
    .sink { print($0) }
RunLoop
// RunLoop 也符合 Scheduler,可在当前 RunLoop 上调度
[1, 2, 3].publisher
    .subscribe(on: RunLoop.current)
    .sink { print($0) }
OperationQueue
// OperationQueue 可作为 Scheduler,可限制并发数
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

[1, 2, 3, 4, 5].publisher
    .subscribe(on: queue)
    .sink { print($0) }

6.2 ImmediateScheduler

立即执行,用于测试。

// ImmediateScheduler:不延迟,立即在当前上下文执行,常用于测试
let scheduler = ImmediateScheduler.shared

[1, 2, 3].publisher
    .receive(on: scheduler)
    .sink { print($0) }

7. 错误处理

7.1 错误类型

// 定义领域错误类型,便于在 Publisher 链中统一处理
enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

func fetchData() -> AnyPublisher<String, NetworkError> {
    // setFailureType:把 Never 等改成指定 Failure 类型;eraseToAnyPublisher 隐藏具体类型
    return Just("数据")
        .setFailureType(to: NetworkError.self)
        .eraseToAnyPublisher()
}

7.2 错误处理策略

// 组合使用:先 catch 兜底、再 retry、最后 replaceError 保证 sink 只收到值
fetchData()
    .catch { error -> Just<String> in
        return Just("默认数据")
    }
    .retry(3)  // 失败时最多重试 3 次
    .replaceError(with: "错误时的默认值")  // 若仍失败,发默认值并正常结束
    .sink { value in
        print(value)
    }

8. 内存管理

8.1 AnyCancellable

保存订阅,防止提前释放。

// 订阅返回 Cancellable,不保存会被立即释放导致订阅断开;用 Set 集中管理
class ViewController {
    private var cancellables = Set<AnyCancellable>()
    
    func setupBinding() {
        $name
            .sink { print($0) }
            .store(in: &cancellables)  // 把订阅存进集合,生命周期与 ViewController 一致
    }
}

8.2 Store 内容管理机制

Store 的作用与原理

store(in:) 方法是 Combine 中管理订阅生命周期的核心机制。理解其工作原理对于正确使用 Combine 至关重要。

8.2.1 AnyCancellable 的本质
// AnyCancellable 是类型擦除的 Cancellable 包装器
public struct AnyCancellable: Cancellable, Hashable {
    private let _cancel: () -> Void
    
    public init(_ cancel: @escaping () -> Void) {
        self._cancel = cancel
    }
    
    public func cancel() {
        _cancel()
    }
    
    // 当 AnyCancellable 被释放时,自动调用 cancel()
    deinit {
        cancel()
    }
}

关键特性:

  • AnyCancellable 是值类型(struct),但内部持有取消操作的闭包
  • AnyCancellable 实例被释放时,会自动调用 cancel() 方法
  • 这确保了订阅在持有者释放时能够正确清理
8.2.2 store(in:) 方法的工作原理
extension Cancellable {
    /// 将 Cancellable 存储到 Set 中,延长其生命周期
    public func store(in set: inout Set<AnyCancellable>) {
        set.insert(AnyCancellable(self))
    }
    
    /// 将 Cancellable 存储到 AnyCancellable 中(单个订阅场景)
    public func store(in cancellable: inout AnyCancellable?) {
        cancellable = AnyCancellable(self)
    }
}

工作流程:

1. 调用 .sink(...) 或 .assign(...) 返回 Cancellable
   ↓
2. 调用 .store(in: &cancellables)
   ↓
3. 将 Cancellable 包装成 AnyCancellable
   ↓
4. 插入到 Set<AnyCancellable> 中
   ↓
5. Set 持有 AnyCancellable,延长订阅生命周期
   ↓
6. 当对象(如 ViewController)释放时,Set 也被释放
   ↓
7. Set 中所有 AnyCancellable 的 deinit 被调用
   ↓
8. 每个 AnyCancellable 的 cancel() 被调用
   ↓
9. 订阅被取消,资源被清理
8.2.3 Set<AnyCancellable> 的管理策略

为什么使用 Set?

// Set 的优势:
// 1. 自动去重(AnyCancellable 实现了 Hashable)
// 2. 高效的插入和查找
// 3. 批量管理多个订阅

class ViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    @Published var email: String = ""
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 多个订阅可以统一管理
        $name
            .sink { print("Name: \($0)") }
            .store(in: &cancellables)
        
        $age
            .sink { print("Age: \($0)") }
            .store(in: &cancellables)
        
        $email
            .sink { print("Email: \($0)") }
            .store(in: &cancellables)
    }
    
    // 当 ViewModel 释放时,所有订阅自动取消
}

生命周期管理示例:

class ViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()
    private let viewModel = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
    }
    
    func setupBindings() {
        // 订阅1:监听数据变化
        viewModel.$data
            .receive(on: DispatchQueue.main)
            .sink { [weak self] data in
                self?.updateUI(with: data)
            }
            .store(in: &cancellables)
        
        // 订阅2:监听错误
        viewModel.$error
            .compactMap { $0 }
            .sink { [weak self] error in
                self?.showError(error)
            }
            .store(in: &cancellables)
        
        // 订阅3:网络请求
        viewModel.fetchData()
            .sink(
                receiveCompletion: { [weak self] completion in
                    if case .failure(let error) = completion {
                        self?.handleError(error)
                    }
                },
                receiveValue: { [weak self] data in
                    self?.handleData(data)
                }
            )
            .store(in: &cancellables)
    }
    
    // 当 ViewController 被释放时:
    // 1. cancellables Set 被释放
    // 2. Set 中所有 AnyCancellable 的 deinit 被调用
    // 3. 所有订阅自动取消,避免内存泄漏
}
8.2.4 手动管理 vs 自动管理

手动管理(不推荐):

class ViewController: UIViewController {
    private var cancellable: AnyCancellable?
    
    func setupBinding() {
        // 需要手动保存,容易忘记
        cancellable = $name
            .sink { print($0) }
        
        // 如果忘记保存,订阅会立即被释放
        $age
            .sink { print($0) }  // ❌ 立即释放,不会收到任何值
    }
    
    // 需要手动取消
    deinit {
        cancellable?.cancel()
    }
}

自动管理(推荐):

class ViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()
    
    func setupBinding() {
        // 自动管理,无需手动 cancel
        $name
            .sink { print($0) }
            .store(in: &cancellables)
        
        $age
            .sink { print($0) }
            .store(in: &cancellables)
        
        // 对象释放时自动清理所有订阅
    }
}
8.2.5 条件性订阅管理

场景:需要动态添加/移除订阅

class ViewModel: ObservableObject {
    @Published var isEnabled: Bool = false
    @Published var data: String = ""
    
    private var cancellables = Set<AnyCancellable>()
    private var dataSubscription: AnyCancellable?
    
    init() {
        // 监听启用状态,动态管理数据订阅
        $isEnabled
            .sink { [weak self] enabled in
                if enabled {
                    self?.startDataSubscription()
                } else {
                    self?.stopDataSubscription()
                }
            }
            .store(in: &cancellables)
    }
    
    private func startDataSubscription() {
        // 创建新的订阅
        dataSubscription = $data
            .sink { print("Data: \($0)") }
        
        // 手动管理单个订阅
        // 注意:这里不使用 store(in: &cancellables),因为需要单独控制
    }
    
    private func stopDataSubscription() {
        // 手动取消订阅
        dataSubscription?.cancel()
        dataSubscription = nil
    }
}

更好的方式:使用条件操作符

class ViewModel: ObservableObject {
    @Published var isEnabled: Bool = false
    @Published var data: String = ""
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 使用 filter 或 flatMap 实现条件订阅,统一管理
        $isEnabled
            .filter { $0 }  // 只在启用时继续
            .flatMap { [weak self] _ -> AnyPublisher<String, Never> in
                guard let self = self else {
                    return Empty().eraseToAnyPublisher()
                }
                return self.$data.eraseToAnyPublisher()
            }
            .sink { print("Data: \($0)") }
            .store(in: &cancellables)
    }
}
8.2.6 Store 的最佳实践

1. 统一管理位置

class ViewModel: ObservableObject {
    // ✅ 推荐:在类的顶部声明,统一管理
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        setupSubscriptions()
    }
    
    private func setupSubscriptions() {
        // 所有订阅都在这里设置
        setupDataSubscription()
        setupErrorSubscription()
    }
}

2. 避免在闭包中创建新的 Set

// ❌ 错误:每次调用都创建新的 Set
func loadData() {
    var cancellables = Set<AnyCancellable>()
    API.fetchData()
        .sink { }
        .store(in: &cancellables)  // 函数返回后立即释放
}

// ✅ 正确:使用实例属性
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func loadData() {
        API.fetchData()
            .sink { }
            .store(in: &cancellables)  // 生命周期与 ViewModel 一致
    }
}

3. 在 SwiftUI 中的使用

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        Text(viewModel.text)
            .onAppear {
                // SwiftUI 中,使用 @StateObject 或 @ObservedObject
                // 订阅会自动管理,但也可以手动管理
                viewModel.$text
                    .sink { print($0) }
                    .store(in: &viewModel.cancellables)
            }
    }
}

class ViewModel: ObservableObject {
    @Published var text: String = ""
    var cancellables = Set<AnyCancellable>()  // 注意:在 SwiftUI 中可能需要 internal
}

4. 测试中的管理

class ViewModelTests: XCTestCase {
    func testSubscription() {
        let viewModel = ViewModel()
        var cancellables = Set<AnyCancellable>()
        var receivedValues: [String] = []
        
        viewModel.$data
            .sink { receivedValues.append($0) }
            .store(in: &cancellables)
        
        viewModel.data = "test"
        
        // 测试完成后,cancellables 会自动清理
        XCTAssertEqual(receivedValues, ["test"])
    }
}
8.2.7 Store 的内部实现细节

AnyCancellable 的 Hashable 实现:

extension AnyCancellable: Hashable {
    public func hash(into hasher: inout Hasher) {
        // 使用对象标识符(ObjectIdentifier)作为哈希值
        // 这确保了每个 AnyCancellable 实例都是唯一的
        hasher.combine(ObjectIdentifier(self))
    }
    
    public static func == (lhs: AnyCancellable, rhs: AnyCancellable) -> Bool {
        // 使用对象标识符比较
        return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
    }
}

为什么 Set 可以自动去重:

// 每个 AnyCancellable 实例都有唯一的对象标识符
// 即使包装相同的 Cancellable,也是不同的 AnyCancellable 实例
let cancellable1 = publisher.sink { }
let cancellable2 = publisher.sink { }

var set = Set<AnyCancellable>()
set.insert(AnyCancellable(cancellable1))  // 插入成功
set.insert(AnyCancellable(cancellable2))  // 插入成功(不同的实例)

// 但如果尝试插入相同的 AnyCancellable:
let anyCancellable = AnyCancellable(cancellable1)
set.insert(anyCancellable)  // 插入成功
set.insert(anyCancellable)  // 插入失败(已存在)
8.2.8 常见错误与解决方案

错误1:忘记 store

// ❌ 错误:订阅立即被释放
func setupBinding() {
    $name.sink { print($0) }  // 立即释放,不会收到任何值
}

// ✅ 正确:使用 store
func setupBinding() {
    $name
        .sink { print($0) }
        .store(in: &cancellables)
}

错误2:在局部作用域中 store

// ❌ 错误:函数返回后 Set 被释放
func loadData() {
    var cancellables = Set<AnyCancellable>()
    API.fetchData()
        .sink { }
        .store(in: &cancellables)
    // 函数返回后,cancellables 被释放,订阅被取消
}

// ✅ 正确:使用实例属性
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func loadData() {
        API.fetchData()
            .sink { }
            .store(in: &cancellables)
    }
}

错误3:循环引用导致无法释放

// ❌ 错误:强引用循环
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func setup() {
        $data.sink { [self] value in  // 强引用 self
            self.process(value)
        }
        .store(in: &cancellables)
        // self → cancellables → AnyCancellable → 闭包 → self(循环)
    }
}

// ✅ 正确:使用 weak self
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func setup() {
        $data.sink { [weak self] value in  // 弱引用
            self?.process(value)
        }
        .store(in: &cancellables)
    }
}

8.3 避免循环引用

// 在 sink 里用到 self 时用 [weak self],避免 self → cancellables → 闭包 → self 的循环
class ViewModel {
    @Published var data: String = ""
    
    func setup() {
        $data
            .sink { [weak self] value in
                self?.process(value)
            }
            .store(in: &cancellables)
    }
    
    private func process(_ value: String) {
        // 处理数据
    }
    
    private var cancellables = Set<AnyCancellable>()
}

9. 实际应用场景

9.1 网络请求

// 使用 dataTaskPublisher 将请求转为 Publisher,再 map/decode 成模型
struct API {
    static func fetchUser(id: Int) -> AnyPublisher<User, Error> {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

API.fetchUser(id: 1)
    .receive(on: DispatchQueue.main)  // 回到主线程再更新 UI
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("错误: \(error)")
            }
        },
        receiveValue: { user in
            print("用户: \(user)")
        }
    )
    .store(in: &cancellables)

9.2 用户输入处理

// 搜索框:防抖 + 去重 + 非空过滤 + flatMap 发请求,结果用 assign 写回 @Published
class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var results: [String] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $searchText
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .removeDuplicates()
            .filter { !$0.isEmpty }
            .flatMap { query -> AnyPublisher<[String], Never> in
                return self.search(query: query)
                    .catch { _ in Just([]) }  // 失败时给空数组,保持 Never
                    .eraseToAnyPublisher()
            }
            .assign(to: \.results, on: self)
            .store(in: &cancellables)
    }
    
    private func search(query: String) -> AnyPublisher<[String], Error> {
        return Just(["结果1", "结果2"])
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

9.3 组合多个数据源

// Zip 等两个请求都完成后再一起处理,适合「同时拉用户与帖子」再更新 UI
class DashboardViewModel: ObservableObject {
    @Published var user: User?
    @Published var posts: [Post] = []
    @Published var isLoading: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    
    func loadData() {
        isLoading = true
        
        let userPublisher = API.fetchUser(id: 1)
        let postsPublisher = API.fetchPosts()
        
        Publishers.Zip(userPublisher, postsPublisher)
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    self?.isLoading = false
                    if case .failure(let error) = completion {
                        print("错误: \(error)")
                    }
                },
                receiveValue: { [weak self] user, posts in
                    self?.user = user
                    self?.posts = posts
                }
            )
            .store(in: &cancellables)
    }
}

10. 更多使用案例

10.1 表单验证(多字段实时校验)

// 用 map 生成错误文案 / 是否有效,assign 到 @Published,实现实时校验
class FormViewModel: ObservableObject {
    @Published var username: String = ""
    @Published var password: String = ""
    @Published var confirmPassword: String = ""
    @Published var isFormValid: Bool = false
    @Published var usernameError: String?
    @Published var passwordError: String?

    private var cancellables = Set<AnyCancellable>()

    init() {
        // 用户名:非空 + 长度,错误信息写回 usernameError
        $username
            .map { name in
                if name.isEmpty { return "请输入用户名" }
                if name.count < 3 { return "至少 3 个字符" }
                return nil
            }
            .assign(to: \.usernameError, on: self)
            .store(in: &cancellables)

        // 三字段 combineLatest,任一变化都重新计算表单是否有效
        Publishers.CombineLatest3($username, $password, $confirmPassword)
            .map { name, pwd, confirm in
                if name.isEmpty || pwd.isEmpty { return false }
                if pwd != confirm { return false }
                if pwd.count < 6 { return false }
                return true
            }
            .assign(to: \.isFormValid, on: self)
            .store(in: &cancellables)
    }
}

10.2 NotificationCenter 转 Publisher

// 系统通知转成 Publisher,再 map 出需要的 payload(如键盘 frame)
let keyboardWillShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    .map { notification in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero
    }
    .receive(on: DispatchQueue.main)

keyboardWillShow
    .sink { frame in
        print("键盘高度: \(frame.height)")
    }
    .store(in: &cancellables)

// 自定义通知名同样用 publisher(for:)
extension Notification.Name {
    static let myCustomEvent = Notification.Name("MyCustomEvent")
}
let customPublisher = NotificationCenter.default.publisher(for: .myCustomEvent)

10.3 Timer 与周期任务

// Timer.publish + autoconnect:按间隔持续发当前日期,需手动 cancel 停止
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
    .autoconnect()
    .sink { date in
        print("tick: \(date)")
    }
// 5 秒后断开
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    timerPublisher.cancel()
}

// 或用 delay + flatMap 递归实现「间隔重复任务」
func repeatingTask(interval: TimeInterval) -> AnyPublisher<Date, Never> {
    Just(Date())
        .delay(for: .seconds(interval), scheduler: DispatchQueue.main)
        .flatMap { _ in repeatingTask(interval: interval) }
        .eraseToAnyPublisher()
}

10.4 SwiftUI 与 @Published 深度绑定

// @Published 变化时同步到 UserDefaults;dropFirst 避免 init 时的初始值触发写入
class SettingsViewModel: ObservableObject {
    @Published var isDarkMode: Bool = false
    @Published var fontSize: Double = 14

    private var cancellables = Set<AnyCancellable>()

    init() {
        $isDarkMode
            .dropFirst()
            .sink { UserDefaults.standard.set($0, forKey: "darkMode") }
            .store(in: &cancellables)

        $fontSize
            .dropFirst()
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .sink { UserDefaults.standard.set($0, forKey: "fontSize") }
            .store(in: &cancellables)
    }
}

// SwiftUI 通过 @ObservedObject 与 $ 绑定,自动刷新
struct SettingsView: View {
    @ObservedObject var viewModel: SettingsViewModel
    var body: some View {
        Toggle("深色模式", isOn: $viewModel.isDarkMode)
        Slider(value: $viewModel.fontSize, in: 10...24)
    }
}

10.5 多源竞速(先到先用)

// 主源失败时用 catch 切到备用源,实现主/备切换
func loadFromPrimaryOrFallback() -> AnyPublisher<Data, Error> {
    let primary = URLSession.shared.dataTaskPublisher(for: primaryURL)
        .map(\.data)
        .mapError { $0 as Error }
    let fallback = URLSession.shared.dataTaskPublisher(for: fallbackURL)
        .map(\.data)
        .mapError { $0 as Error }

    return primary
        .catch { _ in fallback }
        .eraseToAnyPublisher()
}

// 显式 race:merge 后取 first(),即「谁先完成用谁」
extension Publishers {
    static func race<A: Publisher, B: Publisher>(_ a: A, _ b: B) -> AnyPublisher<A.Output, A.Failure>
    where A.Output == B.Output, A.Failure == B.Failure {
        a.merge(with: b)
            .first()
            .eraseToAnyPublisher()
    }
}

10.6 KVO 替代(观察对象属性)

// NSObject + @objc dynamic 可用 .publisher(for:options:) 转成 Combine 流,替代 KVO
class Person: NSObject {
    @objc dynamic var name: String = ""
}

let person = Person()
let namePublisher = person.publisher(for: \.name, options: [.initial, .new])
    .compactMap { $0 as? String }
    .sink { print("name: \($0)") }

person.name = "张三"  // 输出: name: 张三

10.7 请求重试与超时

// timeout:超时未完成则发失败;retry + catch 实现重试与最终兜底
URLSession.shared.dataTaskPublisher(for: url)
    .timeout(.seconds(10), scheduler: DispatchQueue.main)
    .retry(3)
    .map(\.data)
    .decode(type: User.self, decoder: JSONDecoder())
    .catch { error -> Just<User> in
        return Just(User.placeholder)
    }
    .receive(on: DispatchQueue.main)
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { user in
            // 更新 UI
        }
    )
    .store(in: &cancellables)

10.8 节流与防抖组合(搜索 + 连续点击)

// 搜索:防抖,避免每次按键都请求;失败时用 catch 给空数组
$searchText
    .debounce(for: .milliseconds(400), scheduler: RunLoop.main)
    .removeDuplicates()
    .flatMap { query in
        searchAPI(query: query).catch { _ in Just([]) }.eraseToAnyPublisher()
    }
    .receive(on: DispatchQueue.main)
    .assign(to: \.results, on: self)
    .store(in: &cancellables)

// 按钮:节流 1 秒内只响应一次,防止重复提交
buttonTapPublisher
    .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: false)
    .sink { submit() }
    .store(in: &cancellables)