Swift Combine 框架是 Apple 于 2019 年在 WWDC 上发布的,旨在简化响应式编程。响应式编程是一种编程范式,专注于处理异步事件流,例如用户输入、网络请求或文件变化等。Combine 框架的主要目标是通过统一和简化事件处理逻辑,减少传统回调或通知机制的复杂性。
Combine 框架的核心思想是将数据变化表示为“发布者”(Publisher),它可以发出值、完成事件或者错误事件。订阅者(Subscriber)则会订阅这些发布者,以响应数据的变化。Combine 的发布-订阅模式与其他响应式编程库(例如 RxSwift)类似,但它与 Swift 和 Apple 的生态系统高度集成,专门为 iOS、macOS、watchOS 和 tvOS 量身定制。
Combine 的出现解决了开发者在处理异步任务时可能遇到的挑战,使得代码更加简洁、易读和易于维护,同时提供了对 Swift 语言类型系统的强大支持。
核心概念
在 Combine 框架中,处理响应式编程的核心是四个基本概念:Publisher、Subscriber、Subscription 和 Subject。这些概念共同构建了数据流的架构,允许我们在异步事件发生时响应和处理数据。下面详细解释每个概念:
1. Publisher(发布者)
它定义了一个数据源,可以发出值、完成事件或错误。Publisher 会持续向它的订阅者发出这些事件。Publisher 可以发出三种类型的事件:
- 值(Value): 发布的数据。
- 完成(Completion): 事件完成,发布者不会再发出任何事件。
- 错误(Failure): 出现错误,发布者停止发送事件,并向订阅者发送错误。
常见的内置 Publisher 类型:
Just
: 只发布一个值,然后立即完成。Empty
: 不发出任何值,只发送完成或错误事件。Future
: 异步产生一个值,并且只发送一次值或错误。PassthroughSubject
: 允许外部手动发布值,不保留历史值。CurrentValueSubject
: 发布当前值,并对新订阅者发送最新的值。Deferred
: 延迟创建 Publisher,直到有订阅者时才执行。Fail
: 立即发送错误并完成,用于模拟失败。Timer.TimerPublisher
: 定期发送时间值,用于计时或轮询。NotificationCenter.Publisher
: 发布通知中心的通知。URLSession.DataTaskPublisher
: 处理网络请求,发送响应数据或错误。
let publisher = Just(5)
publisher.sink(receiveCompletion: { print($0) }, receiveValue: { print($1) })
2. Subscriber(订阅者)
Subscriber 是 Publisher 的消费者。它接收来自 Publisher 发出的数据或事件,并做出相应处理。订阅者通过订阅(subscribe)特定的 Publisher 来监听它发出的值或状态变化。
Subscriber 的关键在于它的两个方法:
receive(_ input: Input)
: 接收发出的值。receive(completion: Subscribers.Completion<Failure>)
: 接收完成或错误事件。
Combine 提供了 sink
和 assign
等方便的方法来创建订阅者。
let publisher = Just("Hello, Combine")
let subscriber = publisher.sink(
receiveCompletion: { print("Completion: \\($0)") },
receiveValue: { print("Value: \\($0)") }
)
3. Subscription(订阅)
Subscription 是 Subscriber 和 Publisher 之间的桥梁。它管理数据流的生命周期,控制订阅者从发布者接收多少数据。它本质上代表了一个订阅操作。
当订阅者开始监听一个 Publisher 时,Publisher 会创建一个 Subscription,并将它传递给订阅者。Subscriber 可以通过 Subscription 控制是否需要取消订阅或者请求更多数据(例如在背压机制中应用)。
final class CustomSubscriber: Subscriber {
func receive(subscription: Subscription) {
print("Subscribed!")
subscription.request(.max(3)) // 只请求3个值
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received value: \\(input)")
return .none // 不请求更多值
}
func receive(completion: Subscribers.Completion<Never>) {
print("Completed")
}
}
4. Subject(主题)
Subject 是 Combine 框架中特殊类型的 Publisher,它既可以是 Publisher,也可以是 Subscriber。它允许你手动发布值给订阅者,非常适合用于桥接外部事件或多次触发的值。
Combine 中有两种常见的 Subject:
PassthroughSubject
: 不保存任何值,直接将接收到的值发布给订阅者。CurrentValueSubject
: 保存最后发布的一个值,新订阅者会首先接收到当前值。
使用场景:
- 如果你想手动触发某些值或事件,可以使用 Subject。
- 当你想让多个订阅者订阅并立即收到当前最新值时,可以使用
CurrentValueSubject
。
let subject = PassthroughSubject<String, Never>()
let subscriber = subject.sink { value in
print("Received value: \\(value)")
}
subject.send("Hello")
subject.send("World")
操作符 Operators
在 Swift 的 Combine 框架中,Operators(操作符) 是用于对数据流进行处理、转换、过滤等操作的核心组件。操作符可以对从发布者(Publisher)发出的值进行链式处理,类似于函数式编程中的 map、filter 等操作。通过使用操作符,开发者可以灵活地对事件流进行操作,以满足各种需求。
Combine 中的操作符为响应式编程提供了非常强大的数据处理能力。通过这些操作符,开发者可以轻松地处理和转换异步事件流,从而减少代码复杂性并提高代码的可维护性。
以下是 Combine 中常用的一些操作符:
1. Transforming Operators(转换操作符)
map
:将发布者发出的值进行映射,转换成另一个类型的值。let numbers = [1, 2, 3, 4] let publisher = numbers.publisher publisher .map { $0 * 2 } // 将每个值乘以 2 .sink { print($0) }
compactMap
:类似于map
,但可以过滤掉nil
值,只返回非空值。let numbers = ["1", "2", "a", "4"] let publisher = numbers.publisher publisher .compactMap { Int($0) } // 尝试将字符串转换为 Int,忽略不能转换的值 .sink { print($0) }
2. Filtering Operators(过滤操作符)
filter
:只通过满足条件的值,过滤掉不符合条件的元素。let numbers = [1, 2, 3, 4, 5] let publisher = numbers.publisher publisher .filter { $0 % 2 == 0 } // 只允许偶数通过 .sink { print($0) }
removeDuplicates
:移除连续重复的值。let numbers = [1, 1, 2, 3, 3, 3, 4] let publisher = numbers.publisher publisher .removeDuplicates() // 移除连续重复的值 .sink { print($0) }
3. Combining Operators(组合操作符)
merge
:将两个或多个发布者的输出合并成一个流。let pub1 = [1, 2, 3].publisher let pub2 = [4, 5, 6].publisher pub1 .merge(with: pub2) // 将两个发布者的值合并 .sink { print($0) }
combineLatest
:当两个或多个发布者都发出值时,组合它们的最新值。let pub1 = PassthroughSubject<Int, Never>() let pub2 = PassthroughSubject<String, Never>() pub1 .combineLatest(pub2) // 组合最新的值 .sink { print("pub1: \\($0), pub2: \\($1)") }
zip
:与combineLatest
类似,但它只会在两个发布者都有值时才会发出值,并且会按照顺序组合。let pub1 = [1, 2, 3].publisher let pub2 = ["a", "b", "c"].publisher pub1 .zip(pub2) // 按顺序组合两个发布者的值 .sink { print($0, $1) }
4. Timing Operators(时间操作符)
debounce
:延迟发出值,直到指定时间段内没有新的值发出。let subject = PassthroughSubject<String, Never>() subject .debounce(for: .seconds(1), scheduler: DispatchQueue.main) // 1秒内没有值发出才进行发射 .sink { print($0) }
delay
:延迟发布者的输出。let publisher = Just("Hello") publisher .delay(for: .seconds(2), scheduler: DispatchQueue.main) // 延迟2秒后发出值 .sink { print($0) }
5. Error Handling Operators(错误处理操作符)
catch
:处理发布者在发出错误时,返回一个新的发布者。let publisher = Fail<Int, Error>(error: NSError(domain: "", code: -1, userInfo: nil)) publisher .catch { _ in Just(100) } // 捕获错误并返回一个默认值 .sink { print($0) }
retry
:如果发布者遇到错误,可以重新尝试订阅指定次数。let publisher = Fail<Int, Error>(error: NSError(domain: "", code: -1, userInfo: nil)) publisher .retry(3) // 遇到错误时最多重试3次 .sink { print($0) }
6. Reducing Operators(归约操作符)
reduce
:累积值,直到发布者完成并发出最终结果。let numbers = [1, 2, 3, 4] let publisher = numbers.publisher publisher .reduce(0) { $0 + $1 } // 累加所有的值 .sink { print($0) }
scan
:类似于reduce
,但会在每个新值到达时发出中间结果。let numbers = [1, 2, 3, 4] let publisher = numbers.publisher publisher .scan(0) { $0 + $1 } // 累加值并发出中间结果 .sink { print($0) }
高阶 - Scheduler
Scheduler 控制代码执行上下文,决定数据流中发布者和订阅者的运行线程。它是 Combine 中多线程和任务调度的核心,允许指定代码在主线程、后台线程或自定义队列中执行。常见应用:后台网络请求、复杂计算、延迟任务和轻量级频繁任务处理。合理使用 Scheduler 可优化程序性能和用户体验。
Scheduler 的作用
- 线程管理:Scheduler 主要用于控制异步操作在哪个线程执行,特别是在涉及网络请求、文件 I/O 或耗时操作时,确保它们不会阻塞主线程,从而影响 UI 响应。
- 任务调度:通过 Scheduler,可以安排任务的执行顺序,例如:在后台执行复杂操作并在操作完成后切换回主线程更新 UI。
- 频率控制:使用 Scheduler 可以指定执行频率,如在延迟、间隔时间或某个特定的时间后执行任务。
Combine 框架通过使用 subscribe(on:)
和 receive(on:)
来指定发布者和订阅者的执行上下文。
subscribe(on:)
:用于指定数据发布的上下文,控制数据是在哪个 Scheduler(线程)上产生的。receive(on:)
:用于指定数据接收的上下文,控制 Subscriber 接收数据并处理它时在哪个 Scheduler 上运行,常用于将操作结果传回主线程。
典型的 Scheduler 类型
Combine 中的 Scheduler 主要分为几种类型,常用于处理不同场景的调度需求:
DispatchQueue
: 最常用的调度器,允许你在主线程或后台线程上执行任务。RunLoop
: 主要用于事件驱动的循环,适用于任务调度频繁但任务执行轻量的场景。OperationQueue
: 用于管理基于队列的任务执行顺序和优先级。ImmediateScheduler
: 在当前线程上立即执行任务。
典型的应用场景
1. 在后台线程执行网络请求,主线程更新 UI
在 UI 应用中,通常需要在后台执行耗时任务(如网络请求或数据库操作),然后在主线程更新 UI。可以通过 subscribe(on:)
切换到后台线程,处理数据后通过 receive(on:)
切换到主线程更新 UI。
let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "<https://example.com>")!)
.subscribe(on: DispatchQueue.global(qos: .background)) // 在后台执行网络请求
.receive(on: DispatchQueue.main) // 在主线程接收并更新 UI
.sink(receiveCompletion: { completion in
print("Request completed: \\(completion)")
}, receiveValue: { data, response in
print("Data received: \\(data)")
})
2. 延迟任务执行
有时候需要让某些任务延迟执行或在特定时间间隔执行。可以通过 DispatchQueue
调度任务执行。
let publisher = Just("Hello")
.delay(for: .seconds(3), scheduler: DispatchQueue.main) // 延迟3秒后执行
.sink { value in
print("Received after delay: \\(value)")
}
3. 频繁计算任务调度
对于一些频繁且高优先级的任务,比如基于用户输入的实时计算(如实时搜索),可以通过 RunLoop
来执行调度,因为 RunLoop
对于高频率的轻量任务处理效率较高。
let publisher = Just("Search Query")
.receive(on: RunLoop.main) // 在主线程的事件循环中进行
.sink { value in
print("Received on RunLoop: \\(value)")
}
4. 避免主线程阻塞
在执行复杂的计算任务时,使用 subscribe(on:)
将计算操作移至后台,避免阻塞主线程,确保 UI 的流畅性。
let heavyComputation = Just(42)
.map { value in
// 执行一些耗时的计算
return (0..<1000000).reduce(value, +)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) // 在后台线程执行计算
.receive(on: DispatchQueue.main) // 在主线程更新 UI
.sink { result in
print("Computation result: \\(result)")
}
总结
Swift Combine 是 Apple 在 2019 年推出的响应式编程框架,旨在简化异步事件流的处理。Combine 中的核心概念有四个,分别是 发布数据或事件的源头 Publisher、接收来自 Publisher 的数据或事件 的 Subscriber、管理 Publisher 和 Subscriber 之间的关系 的 Subscription 、用于桥接外部数据源并手动控制数据流的 Subject。
除了4个基本概念,还有一些很实用的操作符:
- 转换操作符:如 map、compactMap
- 过滤操作符:如 filter、removeDuplicates
- 组合操作符:如 merge、combineLatest、zip
- 时间操作符:如 debounce、delay
- 错误处理操作符:如 catch、retry
- 归约操作符:如 reduce、scan
Scheduler 是 Combine 的进阶内容,可以简单了解一下,需要的时候再看文档
Combine 框架通过提供统一的事件处理方式,简化了异步编程,提高了代码的可读性和可维护性。它与 Swift 和 Apple 生态系统高度集成,为 iOS、macOS、watchOS 和 tvOS 开发提供了强大的响应式编程工具。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。