目录
[一、Combine 的核心概念](#一、Combine 的核心概念)
[二、使Combine 的基本用法](#二、使Combine 的基本用法)
7.NotificationCenter.Publisher
8.URLSession.DataTaskPublisher
[2. Subscriber(订阅者)](#2. Subscriber(订阅者))
[4. Cancelable(操作符)](#4. Cancelable(操作符))
[1.Cancellable 的用途和作用](#1.Cancellable 的用途和作用)
[2.Cancellable 的实现方式](#2.Cancellable 的实现方式)
[2.管理多个 Cancellable 的集合](#2.管理多个 Cancellable 的集合)
[1.在UIViewController 中使用 Cancellable](#1.在UIViewController 中使用 Cancellable)
[2.Cancellable 总结](#2.Cancellable 总结)
前言
Combine 是 Apple 在 iOS 13 中引入的响应式编程框架,它用于处理 Swift 中的异步操作和数据流。通过 Combine,开发者可以将异步操作(例如网络请求、用户输入、通知等)组合成数据流,并对其进行操作和观察。
网上搜了下Combine的教程,发现要么是翻译的Apple的文档,要么SwiftUI+Combine直接绑定起来使用,这无疑给没有使用过SwiftUI的同学增加了学习的成本,今天决定写一下自己对Combine的理解希望能帮助到准备入门Combine的同学们。
创作不易,您的鼓励和支持是作者创作的不懈动力。
一、Combine 的核心概念
Combine的核心概念主要有Publisher(发布者)、Subscriber(订阅者)、Operator(操作符)、Cancellable。
1.Publisher(发布者)
Publisher是数据的来源,它可以发布一系列的值或者失败的事件。
Publisher 有两种可能的完成方式:成功完成并发送完成事件,或者发送失败事件。
常用的 Publisher 类型有 Just、Future、PassthroughSubject、CurrentValueSubject 等。
2.Subscriber(订阅者)
Subscriber 订阅 Publisher 并接收数据或完成事件。
Combine 提供了内置的 Subscribers.Sink 和 Assign,也可以创建自定义的订阅者。
3.Operator(操作符)
操作符可以对 Publisher 的数据进行转换、过滤、组合等操作,类似于 RxSwift 中的操作符。
常用的操作符有 map、filter、flatMap、removeDuplicates、merge、zip 等。
4.Cancellable
Cancellable 是一个协议,表示订阅可以被取消,从而终止数据流。
通过调用 cancel() 方法可以取消订阅,通常会将订阅存储在 Set<AnyCancellable> 中以便管理。
二、使Combine 的基本用法
1.Publisher(发布者)
1.Just
Just用于立即发布一个特定值,并立即完成。
Just有以下使用场景:
1.调试与测试:在开发过程中临时显示某些静态数据,方便调试数据管道
2.占位符数据:在数据加载之前,可以用静态文本作为提示或者占位符,以提升用户体验。
3.初始化静态状态:例如用于显示固定的欢迎消息、错误消息等单次信息展示。
例如在下面的例子中,刚进入页面的时候有一个占位字符串,当我们点击按钮之后,使用真实的提示符代替占位字符串。
图1.Just的实例
2.Future
Future 是 Combine 中的一个发布者,用于在未来某个时刻异步返回一个值(或者失败)。它常用于封装异步操作,例如网络请求、文件读取等。
下面的例子中,我们使用一个按钮触发模拟的网络请求,并将结果显示在屏幕上。
Swift
import UIKit
import Combine
class FutureDemoViewController: UIViewController {
private var cancellables = Set<AnyCancellable>() // 用于管理订阅
private let resultLabel = UILabel() // 用于显示请求结果
private let requestButton = UIButton(type: .system) // 请求按钮
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .white
self.title = "Future 使用示例"
// 配置按钮
requestButton.setTitle("发送请求", for: .normal)
requestButton.addTarget(self, action: #selector(makeRequest), for: .touchUpInside)
// 配置结果标签
resultLabel.text = "等待请求结果..."
resultLabel.textAlignment = .center
// 添加子视图并布局
view.addSubview(resultLabel)
view.addSubview(requestButton)
resultLabel.translatesAutoresizingMaskIntoConstraints = false
requestButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
resultLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
requestButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
requestButton.topAnchor.constraint(equalTo: resultLabel.bottomAnchor, constant: 20)
])
}
@objc private func makeRequest() {
fetchDataFromServer()
.receive(on: DispatchQueue.main) // 确保在主线程更新 UI
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("请求完成")
case .failure(let error):
self.resultLabel.text = "请求失败: \(error.localizedDescription)"
}
}, receiveValue: { value in
self.resultLabel.text = "请求结果: \(value)"
})
.store(in: &cancellables)
}
/// 模拟从服务器获取数据的 Future
private func fetchDataFromServer() -> Future<String, Error> {
return Future { promise in
print("开始请求数据...")
DispatchQueue.global().asyncAfter(deadline: .now() + 2) { // 模拟网络延迟
let success = Bool.random() // 随机成功或失败
if success {
promise(.success("请求成功:返回的数据"))
} else {
promise(.failure(NSError(domain: "网络错误", code: -1, userInfo: nil)))
}
}
}
}
}
3.Deferred
Deferred 是 Combine 中的一个发布者,用于延迟创建和执行发布者。它会在每次订阅时创建一个新的发布者实例,这在需要动态生成发布者,或需要在订阅时才初始化发布者的场景中非常有用。
我们来创建一个示例,用 Deferred 模拟网络请求,每次点击按钮时延迟执行并返回结果。
Swift
import UIKit
import Combine
class DeferredDemoViewController: UIViewController {
private var cancellables = Set<AnyCancellable>() // 管理订阅
private let resultLabel = UILabel() // 显示请求结果
private let requestButton = UIButton(type: .system) // 请求按钮
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .white
self.title = "Deferred 使用示例"
// 设置按钮
requestButton.setTitle("发送请求", for: .normal)
requestButton.addTarget(self, action: #selector(makeRequest), for: .touchUpInside)
// 设置结果标签
resultLabel.text = "等待请求结果..."
resultLabel.textAlignment = .center
// 添加视图并设置布局
view.addSubview(resultLabel)
view.addSubview(requestButton)
resultLabel.translatesAutoresizingMaskIntoConstraints = false
requestButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
resultLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
requestButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
requestButton.topAnchor.constraint(equalTo: resultLabel.bottomAnchor, constant: 20)
])
}
@objc private func makeRequest() {
createDeferredPublisher()
.receive(on: DispatchQueue.main) // 主线程更新 UI
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("请求完成")
case .failure(let error):
self.resultLabel.text = "请求失败: \(error.localizedDescription)"
}
}, receiveValue: { value in
self.resultLabel.text = "请求结果: \(value)"
})
.store(in: &cancellables)
}
/// 使用 Deferred 创建一个新的延迟发布者
private func createDeferredPublisher() -> AnyPublisher<String, Error> {
return Deferred {
Future { promise in
print("开始请求数据...")
DispatchQueue.global().asyncAfter(deadline: .now() + 2) { // 模拟网络延迟
let success = Bool.random() // 随机成功或失败
if success {
promise(.success("请求成功:返回的数据"))
} else {
promise(.failure(NSError(domain: "网络错误", code: -1, userInfo: nil)))
}
}
}
}
.eraseToAnyPublisher()
}
}
4.Empty
Empty 是 Combine 中的一个发布者,专门用于立即完成并不会发布任何值的情况。它常用于不需要发送数据的场景,或者只是为了提供一个结束事件(完成或失败)。下面是一个示例,展示如何使用 Empty 来表示无数据的情形。
5.Fail
Fail用于发布错误,主要用于模拟失败场景。
非常适合测试错误处理。
let failPublisher = Fail<Int, NSError>(error: NSError(domain: "Error", code: -1, userInfo: nil))
failPublisher.sink(receiveCompletion: { print($0) }, receiveValue: { _ in print("Received") })
// 输出: failure(Error Domain=Error Code=-1 "(null)")
6.Timer.TimerPublisher
生成定时事件流的发布者,常用于需要定时更新的 UI 场景。
可以指定时间间隔来发布数据。
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .default)
let cancellable = timerPublisher.autoconnect()
.sink { time in print("Current time: \(time)") }
7.NotificationCenter.Publisher
从系统或自定义通知发布数据。
可以监听系统通知或应用内自定义通知。
let notificationPublisher = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
notificationPublisher.sink { _ in print("App entered background") }
8.URLSession.DataTaskPublisher
专用于执行网络请求,发布请求结果或错误。
适用于网络请求的响应式处理。
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for: url)
dataTaskPublisher
.map { $0.data }
.decode(type: Post.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print($0) }, receiveValue: { post in print(post) })
9.Publishers.Sequence
将数组或集合等序列转换为发布者,顺序发布每个元素。
适合在数据管道中逐个发布已有的数组数据。
let sequencePublisher = Publishers.Sequence(sequence: [1, 2, 3, 4, 5])
sequencePublisher.sink(receiveCompletion: { print($0) }, receiveValue: { print($1) })
// 输出: 1, 2, 3, 4, 5, finished
10.PassthroughSubject
用于创建手动控制的数据流,可以动态发布数据。
适用于需要手动触发事件的场景,比如按钮点击。
let passthroughSubject = PassthroughSubject<String, Never>()
passthroughSubject.sink(receiveValue: { print($0) })
passthroughSubject.send("Hello")
passthroughSubject.send("World")
// 输出: Hello, World
11.CurrentValueSubject
与 PassthroughSubject 类似,但保存并发布最新的值。
适用于需要随时读取最新值的场景(类似于变量)。
let currentValueSubject = CurrentValueSubject<Int, Never>(10)
currentValueSubject.sink { print("Current value: \($0)") }
currentValueSubject.send(20)
currentValueSubject.send(30)
// 输出: Current value: 10, Current value: 20, Current value: 30
2. Subscriber(订阅者)
1.sink
sink 是 Combine 中最常用的订阅者。它允许我们在闭包中定义如何处理发布者发出的数据以及完成或失败的事件。它的两种常见形式分别是:
1.接受数据与完成事件:同时订阅值和完成事件.
2.只接受数据**:**仅订阅值更新,适合只需要处理数据而忽略完成状态的场景.
import Combine
let publisher = Just("Hello, Combine!")
let cancellable = publisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("完成")
case .failure(let error):
print("错误:\(error)")
}
},
receiveValue: { value in
print("接收到值:\(value)")
}
)
在这个例子中,sink 会接收 Just 发布者发出的字符串 "Hello, Combine!",并在控制台输出。
2.assign
assign 是一个特殊的订阅者,用于将发布者发出的值直接赋值给对象的属性。通常用于 UI 更新,尤其是在 SwiftUI 或 UIKit 中,将发布的数据自动绑定到 UI 组件上。
import Combine
class ExampleViewModel: ObservableObject {
@Published var text: String = ""
}
let viewModel = ExampleViewModel()
let publisher = Just("Updated Text")
let cancellable = publisher.assign(to: \.text, on: viewModel)
print(viewModel.text) // 输出: Updated Text
在这个示例中,assign 将 Just 发布者发出的字符串直接赋值给 viewModel 的 text 属性。这种方式简化了数据流到属性的赋值操作。
3.自定义订阅者 (Subscriber)
Combine 允许我们创建自定义订阅者,以便完全控制数据接收和完成处理的方式。实现自定义订阅者需要遵循 Subscriber 协议。自定义订阅者适合于更复杂的需求,例如控制数据接收频率或缓存数据。
import Combine
class CustomSubscriber: Subscriber {
typealias Input = String
typealias Failure = Never
func receive(subscription: Subscription) {
print("订阅开始")
subscription.request(.max(1)) // 请求一个数据
}
func receive(_ input: String) -> Subscribers.Demand {
print("接收到数据:\(input)")
return .none // 不请求更多数据
}
func receive(completion: Subscribers.Completion<Never>) {
print("完成")
}
}
let publisher = Just("Hello, Custom Subscriber!")
let customSubscriber = CustomSubscriber()
publisher.subscribe(customSubscriber)
在这个例子中,CustomSubscriber 实现了 Subscriber 协议的三个方法。receive(subscription:) 请求一个数据项,receive(_:) 接收到发布的数据,receive(completion:) 处理完成状态。
3.Operator(操作符)
1.map
map 用于对每个值进行转换,将一个数据类型映射为另一个数据类型,适合简单的映射操作。
import Combine
let publisher = [1, 2, 3, 4, 5].publisher
let cancellable = publisher
.map { $0 * 2 } // 每个值乘以 2
.sink { value in
print(value) // 输出:2, 4, 6, 8, 10
}
2.filter
filter 用于筛选满足条件的值,只发出符合条件的元素,适合条件筛选的场景。
import Combine
let publisher = [1, 2, 3, 4, 5].publisher
let cancellable = publisher
.filter { $0 % 2 == 0 } // 只发出偶数
.sink { value in
print(value) // 输出:2, 4
}
3.removeDuplicates
removeDuplicates 用于移除连续的重复值,只保留与上一个值不相同的元素,适合处理去重需求。
import Combine
let publisher = [1, 2, 2, 3, 3, 3, 4].publisher
let cancellable = publisher
.removeDuplicates()
.sink { value in
print(value) // 输出:1, 2, 3, 4
}
4.combineLatest
combineLatest 用于组合两个发布者的最新值,当其中一个发布者发出新值时,都会以最新的组合值发出,适合多数据流同步处理。
import Combine
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let cancellable = publisher1
.combineLatest(publisher2)
.sink { value in
print("组合的最新值:\(value)") // 输出最新组合值
}
publisher1.send("A")
publisher2.send(1) // 输出: ("A", 1)
publisher1.send("B") // 输出: ("B", 1)
5.merge
merge 将多个发布者的输出合并成一个流,只要任意发布者发出值,合并后的发布者就会发出该值,适合多个数据流合并的场景。
import Combine
let publisher1 = [1, 2, 3].publisher
let publisher2 = [4, 5, 6].publisher
let cancellable = publisher1
.merge(with: publisher2)
.sink { value in
print(value) // 输出:1, 2, 3, 4, 5, 6
}
6.debounce
debounce 会在一段时间内没有新的值发出时才发出最后一个值,常用于处理快速连续的事件(例如用户输入),防止过于频繁的触发。
import Combine
import Foundation
let subject = PassthroughSubject<String, Never>()
let cancellable = subject
.debounce(for: .seconds(1), scheduler: DispatchQueue.main) // 1秒内无新值时才发送
.sink { value in
print(value)
}
subject.send("A")
subject.send("B") // 快速连续的值 "A" 和 "B",只会输出 "B"
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
subject.send("C") // 1.5秒后发送 "C",会立即输出
}
7.throttle
throttle 类似于 debounce,但会定期发出最新的值,而不管该期间内是否有值更新,适合限制数据频率的场景。
8.catch
catch 捕获并处理错误,允许在遇到错误时提供备用的发布者进行替代操作,适合处理失败后的回退逻辑。
import Combine
let publisher = Fail<String, Error>(error: URLError(.badURL))
let cancellable = publisher
.catch { _ in Just("备用值") } // 捕获错误并提供一个替代值
.sink { value in
print(value) // 输出:备用值
}
4. Cancelable(操作符)
在 Swift 的 Combine 框架中,Cancellable 是一个非常重要的协议,用于管理和取消订阅。每当我们订阅了一个 Publisher,Combine 会返回一个符合 Cancellable 协议的实例。我们可以通过这个实例在适当的时候取消订阅,从而停止数据流的处理,避免资源浪费和内存泄漏。
1.Cancellable 的用途和作用
在 Combine 中,数据流是由发布者(Publisher)和订阅者(Subscriber)之间的订阅关系建立的。在某些情况下,订阅者可能只需要从发布者接收有限时间的数据流,比如:
1.用户离开当前页面,数据不再需要更新
2.网络请求超时或任务已经完成
3.用户主动取消操作
此时,Cancellable 提供了一种取消订阅的方法,使得我们可以手动停止数据流,释放资源。
2.Cancellable 的实现方式
所有符合 Cancellable 协议的对象都有一个 cancel() 方法,用于取消订阅关系。以下是一些常见的用法和示例:
1.基础用法
创建发布者并订阅它,然后使用 Cancellable 来管理订阅状态。
import Combine
let publisher = ["Hello", "Combine", "Framework"].publisher
// 订阅并获取 Cancellable 实例
let subscription: Cancellable = publisher.sink { value in
print("Received value: \(value)")
}
// 取消订阅
subscription.cancel()
在上面的例子中,subscription.cancel() 被调用后,发布者将不再发送数据给订阅者。
2.管理多个 Cancellable 的集合
在实际项目中,我们通常会有多个数据流的订阅。因此,可以使用 AnyCancellable 和 Set<AnyCancellable> 来管理多个订阅关系,以便在适当时机一并取消。
import Combine
var cancellables = Set<AnyCancellable>()
let publisher1 = ["A", "B", "C"].publisher
let publisher2 = [1, 2, 3].publisher
publisher1
.sink { value in
print("Publisher1 received: \(value)")
}
.store(in: &cancellables) // 自动存储到 cancellables 集合
publisher2
.sink { value in
print("Publisher2 received: \(value)")
}
.store(in: &cancellables) // 自动存储到 cancellables 集合
// 在适当时机清空集合,取消所有订阅
cancellables.removeAll()
在上面的代码中,所有存储在 cancellables 集合中的 Cancellable 都会被 removeAll 取消。
3.使用AnyCancellable自动取消
AnyCancellable 是一个特定的 Cancellable 类型,可以在超出范围时自动取消订阅。因此,我们不必手动调用 cancel(),它会在生命周期结束时自动执行。
import Combine
let publisher = ["Hello", "World"].publisher
let cancellable = publisher.sink { value in
print("Received value: \(value)")
}
do {
let anyCancellable = AnyCancellable {
print("Subscription is cancelled")
}
// `anyCancellable` 超出作用域后会自动执行 cancel
}
4.常见使用场景
1.在UIViewController 中使用 Cancellable
在 UIViewController 中,可以使用 Set<AnyCancellable> 存储所有的订阅,并在 viewDidDisappear 或者 deinit 中进行取消:
import Combine
import UIKit
class MyViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
let publisher = ["Combine", "is", "powerful"].publisher
publisher
.sink { value in
print("Received value: \(value)")
}
.store(in: &cancellables) // 自动管理取消
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
cancellables.removeAll() // 页面消失时取消所有订阅
}
}
在这个例子中,页面消失时 cancellables 被清空,所有的订阅都会被取消,确保不会有多余的资源占用。
2.Cancellable 总结
-
管理数据流:在 Combine 中,Cancellable 是管理数据流的重要工具。
-
避免资源泄漏:当不再需要数据流时,可以通过 cancel() 取消订阅,释放资源。
-
集合管理:使用 Set<AnyCancellable> 可以方便地管理多个订阅,适用于控制器等复杂场景。
-
作用域管理:AnyCancellable 可以自动管理作用域内的取消,适合临时订阅的场景。