在Swift中,Combine 框架是在 iOS 13 及更高版本中推出的,Combine框架是一个用于处理异步编程的模式,它提供了一套强大的工具,用于组合异步事件流(例如网络请求、用户输入、定时器等)。让代码更加简洁、易读和可维护。它的核心在于 Publisher-Subscriber 模式以及强大的操作符链,能够高效地处理各种异步场景。
一、核心概念
- Publisher:作为数据流的源头,它会产生一系列事件。
- Subscriber:负责接收 Publisher 发出的事件。
- Operator:属于中间处理节点,能够对数据流进行转换、过滤等操作。
- Subscription:用于管理 Publisher 和 Subscriber 之间的连接。
二、主要组件
1.Publisher
Publisher
是一个可以发送值、完成信号或错误的类型。它代表了异步数据流的生产者。例如,一个网络请求可以是一个Publisher
,它会发送接收到的数据,或者在请求失败时发送错误。
有两种基本类型:
Just
:仅发送单个值然后结束。Empty
:不发送任何值就直接结束。
另外,很多 Swift 原生类型都实现了 Publisher 协议,比如Array
和URLSession
。
let publisher1 = Just("Hello ,Combine")
let subscriber1 = publisher1.sink { completion in
print("完成:", completion)
} receiveValue: { receiveValue in
print("接收到值:", receiveValue)
sleep(3)//添加3秒延迟,在3秒后才走到completion里面
}
let publisher2 = Empty<Any, Never>()
let subscriber2 = publisher2.sink { completion in
print("空发布者完成状态: \(completion)")
} receiveValue: { _ in
// 此闭包不会执行
print("接收到值")
}
/**
打印
接收到值: Hello ,Combine
完成: finished
*/
/**
打印
空发布者完成状态: finished
*/
let failPublisher1 = Fail<Int,CustomError>(error: .sampleError)
failPublisher1.sink { completion in
if case let .failure(error) = completion {
print("接收到的错误: \(error)")
}
} receiveValue: { _ in
// 此闭包不会执行
}
/**
打印
接收到的错误: sampleError
*/
2.Subscriber
Subscriber
是一个可以接收来自Publisher
值的类型。它定义了如何处理这些值(例如,更新UI或处理数据)。
Sink
:通过闭包来处理接收到的值和完成事件。Assign
:可以将接收到的值赋给对象的属性。
Subscriber
是一个协议,定义了订阅者必须实现的方法,用于与发布者建立连接并处理其发送的内容:
public protocol Subscriber {
// 订阅者接收的值的类型
associatedtype Input
// 订阅者可能接收的错误类型(必须遵循 Error 协议)
associatedtype Failure: Error
// 发布者调用此方法,向订阅者发送值
func receive(_ input: Input) -> Subscribers.Demand
// 发布者调用此方法,向订阅者发送完成信号(成功或失败)
func receive(completion: Subscribers.Completion<Failure>)
// 发布者调用此方法,与订阅者建立正式连接
func receive(subscription: Subscription)
}
关键角色
- 接收数据:通过
receive(_:)
方法接收发布者发送的单个值。 - 处理完成:通过
receive(completion:)
方法处理发布者的终止信号(.finished
或.failure
)。 - 建立连接:通过
receive(subscription:)
方法接收Subscription
对象,用于控制数据需求(如请求更多数据)。 - 控制需求:
receive(_:)
方法的返回值Subscribers.Demand
用于告诉发布者还能接收多少数据(背压控制)。
常用订阅者实现
1. sink
:闭包式订阅者
最常用的订阅方式,通过闭包处理接收的值和完成事件:
let publisher = [1, 2, 3].publisher
// 使用 sink 订阅
let cancellable = publisher.sink { completion in
switch completion {
case .finished:
print("数据流完成")
case .failure(let error):
print("发生错误:\(error)")
}
} receiveValue: { value in
print("接收的值:\(value)")
}
// 输出:
// 接收的值:1
// 接收的值:2
// 接收的值:3
// 数据流完成
2. assign
:绑定属性的订阅者
1. assign(to:on:)
基础示例
import Combine
class User {
var name: String = "默认名称"
var age: Int = 0
}
let user = User()
let namePublisher = ["Alice", "Bob", "Charlie"].publisher
let agePublisher = (20...22).publisher
// 将名字绑定到 user.name
let nameCancellable = namePublisher
.assign(to: \.name, on: user)
.store(in: &cancellables)
// 将年龄绑定到 user.age
let ageCancellable = agePublisher
.assign(to: \.age, on: user)
.store(in: &cancellables)
print(user.name) // 输出:Charlie(最后一个值)
print(user.age) // 输出:22(最后一个值)
2. assign(to:)
示例(Swift 5.5+)
在当前作用域中直接绑定属性:
class ProfileViewModel {
var username: String = ""
var cancellables = Set<AnyCancellable>()
func setup() {
// 模拟网络请求获取用户名
Just("SwiftDeveloper")
.assign(to: &$username) // 直接绑定到当前对象的 username 属性
}
}
线程注意事项:默认在发布者发送值的线程更新属性,如果需要在主线程更新 UI,需配合 receive(on:)
:
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: DispatchQueue.main) // 切换到主线程
.assign(to: \.data, on: self)
.store(in: &cancellables)
错误处理
assign
要求发布者的错误类型必须是 Never
(即不会发送错误)。如果发布者可能发送错误,需要先通过 catch
等操作符处理:
enum MyError: Error {
case invalidData
}
// 可能发送错误的发布者
let failingPublisher = Fail<String, MyError>(error: .invalidData)
// 先处理错误,再使用 assign
failingPublisher
.catch { _ in Just("默认值") } // 将错误转换为默认值
.assign(to: \.name, on: user)
.store(in: &cancellables)
手动实现 Subscriber
协议:
import Combine
// 自定义订阅者:只接收偶数
class EvenNumberSubscriber: Subscriber {
// 接收 Int 类型的值,不处理错误
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
// 初始请求 2 个值
subscription.request(.max(2))
}
func receive(_ input: Int) -> Subscribers.Demand {
if input % 2 == 0 {
print("接收偶数:\(input)")
// 每接收一个偶数,再请求 1 个值
return .max(1)
} else {
print("忽略奇数:\(input)")
// 忽略奇数时不请求新值
return .none
}
}
func receive(completion: Subscribers.Completion<Never>) {
print("完成:\(completion)")
}
}
// 使用自定义订阅者
let numbers = [1, 2, 3, 4, 5, 6].publisher
let subscriber = EvenNumberSubscriber()
numbers.subscribe(subscriber)
// 输出:
// 忽略奇数:1
// 接收偶数:2
// 忽略奇数:3
// 接收偶数:4
// 忽略奇数:5
// 接收偶数:6
// 完成:finished
背压控制(Demand)
Subscribers.Demand
用于控制订阅者能接收的数据量,避免数据过载,常见类型:
.none
:不请求更多数据.max(n)
:最多再接收n
个数据.unlimited
:接收无限量数据(默认行为)
例如,在 receive(subscription:)
中设置初始需求:
func receive(subscription: Subscription) {
// 初始请求 10 个数据
subscription.request(.max(10))
}
生命周期管理
订阅者必须通过 AnyCancellable
管理生命周期,否则可能导致提前取消或内存泄漏:
class MyViewModel {
private var cancellables = Set<AnyCancellable>()
func setup() {
Just("Hello")
.sink { print($0) }
.store(in: &cancellables) // 自动管理订阅生命周期
}
}
3.Operator
Operator
是对Publisher
的操作,它允许你转换、过滤、组合和响应数据流。例如,你可以使用map
来转换数据流中的值,使用filter
来过滤值,或者使用combineLatest
来组合多个数据流。
- 转换类:像
map
、flatMap
等。 - 过滤类:例如
filter
、removeDuplicates
。 - 合并类:如
zip
、merge
。 - 时间类:像
debounce
、throttle
。
1. 转换操作符(Transforming)
对数据流中的值进行转换处理。
操作符 | 作用 | 示例 |
map | 将值从一种类型转换为另一种类型 | [1,2,3].publisher.map { $0 * 2 } → 输出 2,4,6 |
flatMap | 将值转换为新的发布者,并合并其输出 | 嵌套发布者展平处理 |
compactMap | 过滤 nil 并解包可选值 | [1,nil,3].publisher.compactMap { $0 } → 输出 1,3 |
mapError | 将错误类型转换为另一种错误类型 | 统一错误处理 |
示例:flatMap
处理嵌套发布者
import Combine
// 模拟每个用户ID对应的详情请求
func fetchUserDetails(id: Int) -> AnyPublisher<String, Never> {
Just("User \(id) Details")
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
// 转换用户ID流为用户详情流
let userIds = [1, 2, 3].publisher
userIds
.flatMap { id in
fetchUserDetails(id: id)
}
.sink { print($0) }
.store(in: &cancellables)
// 输出(间隔1秒):
// User 1 Details
// User 2 Details
// User 3 Details
2. 过滤操作符(Filtering)
筛选或限制数据流中的值。
操作符 | 作用 | 示例 |
filter | 只保留满足条件的值 | [1,2,3,4].publisher.filter { $0 % 2 == 0 } → 输出 2,4 |
removeDuplicates | 过滤连续重复的值 | [1,1,2,2,3].publisher.removeDuplicates() → 输出 1,2,3 |
first(where:) | 只取第一个满足条件的值 | 取第一个偶数 |
last(where:) | 只取最后一个满足条件的值 | 取最后一个偶数 |
prefix | 只取前 n 个值 | prefix(2) 取前两个值 |
示例:filter
与 removeDuplicates
let numbers = [1, 2, 2, 3, 4, 4, 5].publisher
numbers
.filter { $0 > 2 } // 保留大于2的值
.removeDuplicates() // 去重连续重复值
.sink { print($0) } // 输出:3,4,5
.store(in: &cancellables)
3. 组合操作符(Combining)
将多个数据流合并或关联。
操作符 | 作用 | 示例 |
merge | 合并多个同类型发布者的输出 | 合并两个整数流 |
zip | 按顺序配对多个发布者的值(如第 n 个值配对) | 配对姓名和年龄流 |
combineLatest | 当任意一个发布者发送新值时,组合所有发布者的最新值 | 实时组合多个输入框的内容 |
prepend | 在数据流前添加值或其他发布者的输出 | 在现有流前插入初始值 |
示例:combineLatest
实时组合数据
let username = PassthroughSubject<String, Never>()
let password = PassthroughSubject<String, Never>()
// 组合最新的用户名和密码
username
.combineLatest(password)
.sink { username, password in
print("当前输入: 用户名=\(username), 密码=\(password)")
}
.store(in: &cancellables)
username.send("alice") // 输出:当前输入: 用户名=alice, 密码=(等待密码)
password.send("123") // 输出:当前输入: 用户名=alice, 密码=123
username.send("bob") // 输出:当前输入: 用户名=bob, 密码=123
4. 时序操作符(Timing)
控制数据发送的时间或节奏。
操作符 | 作用 | 示例 |
delay | 延迟发送所有值 | 延迟 1 秒发送 |
debounce | 等待指定时间无新值后,只发送最后一个值 | 搜索输入防抖(停止输入后再请求) |
throttle | 指定时间内只发送第一个 / 最后一个值 | 按钮点击节流 |
measureInterval | 测量值之间的时间间隔 | 计算帧率 |
let searchInput = PassthroughSubject<String, Never>()
// 输入停止0.5秒后才发送请求
searchInput
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink { query in
print("搜索请求: \(query)")
}
.store(in: &cancellables)
// 模拟快速输入
searchInput.send("sw")
searchInput.send("swi")
searchInput.send("swif") // 0.5秒内无新输入,最终发送 "swif"
5. 错误处理操作符(Error Handling)
处理数据流中的错误。
操作符 | 作用 | 示例 |
catch | 捕获错误并返回备用发布者 | 网络错误时返回本地缓存 |
replaceError | 用默认值替换错误 | 错误时返回空字符串 |
retry | 错误时重试指定次数 | 网络请求失败重试 3 次 |
mapError | 将错误类型转换为另一种 | 统一错误类型 |
示例:retry
与 catch
处理网络错误
enum NetworkError: Error {
case timeout
}
// 模拟可能失败的网络请求
func fetchData() -> AnyPublisher<String, NetworkError> {
var attempt = 0
return Future { promise in
attempt += 1
print("尝试第\(attempt)次请求")
if attempt < 3 {
promise(.failure(.timeout)) // 前2次失败
} else {
promise(.success("数据加载成功")) // 第3次成功
}
}
.eraseToAnyPublisher()
}
fetchData()
.retry(2) // 失败时重试2次(共3次尝试)
.catch { error in
Just("使用缓存数据") // 仍失败则返回缓存
}
.sink { print($0) }
.store(in: &cancellables)
// 输出:
// 尝试第1次请求
// 尝试第2次请求
// 尝试第3次请求
// 数据加载成功
6. 生命周期操作符(Lifecycle)
控制数据流的生命周期。
操作符 | 作用 | 示例 |
eraseToAnyPublisher | 隐藏发布者具体类型,返回 AnyPublisher | 封装内部实现,暴露统一接口 |
breakpoint | 调试用,满足条件时触发断点 | 特定值时中断调试 |
handleEvents | 监听数据流各阶段事件(订阅、接收值、完成等) | 打印日志或执行副作用 |
示例:handleEvents
监听数据流事件
[1,2,3].publisher
.handleEvents(
receiveSubscription: { _ in print("开始订阅") },
receiveOutput: { value in print("即将发送: \(value)") },
receiveCompletion: { _ in print("流完成") },
receiveCancel: { print("订阅被取消") },
receiveRequest: { demand in print("请求数据量: \(demand)") }
)
.sink { print("接收值: \($0)") }
.store(in: &cancellables)
操作符链的执行逻辑
操作符链式调用时,数据会按顺序流经每个操作符:
publisher
.map { $0 * 2 } // 1. 先转换
.filter { $0 > 5 } // 2. 再过滤
.debounce(...) // 3. 再控制时序
.sink { ... } // 4. 最后处理
每个操作符都会返回一个新的发布者,因此链中的每个步骤都是上下游关系。
合理使用操作符可以简化异步代码,避免 “回调地狱”,使逻辑更易读、易维护。
4.Subject
Subject
是Publisher
和Subscriber
的结合体,它既可以发送值也可以接收值。它可以用来在数据流中注入新的值或者控制何时发送值。例如,一个PassthroughSubject
可以作为一个手动控制的异步事件源。
PassthroughSubject
:只会转发接收到的最新值。CurrentValueSubject
:会存储当前值,并且会向新的订阅者发送这个当前值。
4.1 PassthroughSubject
//手动控制发布者
let subject = PassthroughSubject<String, CustomError>()
let subscription = subject.sink { completion in
print("订阅1完成:", completion)
} receiveValue: { value in
print("订阅1接收到:", value)
}
subject.send("第一个消息") // 输出:订阅1接收到: 第一个消息
let subscription2 = subject.sink { completion in
print("订阅2完成:", completion)
} receiveValue: { value in
print("订阅2接收到:", value)
}
subject.send("第二个消息") // 输出:订阅1接收到: 第二个消息,订阅2接收到: 第二个消息
//.finished和.failure二选一
//subject.send(completion: .failure(.networkFailure))
subject.send(completion: .finished)
打印:
订阅1接收到: 第一个消息
订阅2接收到: 第二个消息
订阅1接收到: 第二个消息
订阅2完成: finished
订阅1完成: finished
实际应用场景
1. 事件传递
适合传递一次性事件(如按钮点击、状态变化通知等)
class ButtonViewModel {
// 按钮点击事件流
private let tapSubject = PassthroughSubject<Void, Never>()
// 提供外部订阅接口
var tapPublisher: AnyPublisher<Void, Never> {
tapSubject.eraseToAnyPublisher()
}
// 模拟按钮点击
func buttonTapped() {
tapSubject.send(())
}
}
// 使用
let viewModel = ButtonViewModel()
viewModel.tapPublisher
.sink {
print("按钮被点击了")
}
.store(in: &cancellables)
viewModel.buttonTapped() // 输出:按钮被点击了
2.数据转发
作为数据流转节点,转发其他发布者的数据:
// 模拟网络请求发布者
let networkPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com")!)
.map { $0.data }
.eraseToAnyPublisher()
// 创建PassthroughSubject转发数据
let dataSubject = PassthroughSubject<Data, URLError>()
// 订阅网络请求并转发数据
networkPublisher
.subscribe(dataSubject)
.store(in: &cancellables)
// 订阅subject获取数据
dataSubject
.sink(
receiveCompletion: { completion in
print("数据接收完成:\(completion)")
},
receiveValue: { data in
print("收到数据:\(data.count)字节")
}
)
.store(in: &cancellables)
4.2 CurrentValueSubject
let currentSubject = CurrentValueSubject<String, CustomError>("初始值")
// 第一个订阅者
let subscriber1 = currentSubject.sink { completion in
print("订阅1完成:", completion)
} receiveValue: { value in
print("订阅1接收到:", value)
}
// 输出:订阅者1收到:初始值(新订阅者立即收到当前值)
// 发送新值
currentSubject.send("更新值1")
// 输出:订阅者1收到:更新值1
let subscriber2 = currentSubject.sink { completion in
print("订阅2完成:", completion)
} receiveValue: { value in
print("订阅2接收到:", value)
}
// 输出:订阅1接收到: 更新值1 订阅者2收到:更新值1(新订阅者收到当前值)
// 直接访问当前值
print("当前值:\(currentSubject.value)") // 输出:当前值:更新值1
// 发送完成信号
currentSubject.send(completion: .finished)
// 之后的发送会被忽略
currentSubject.send("更新值2") // 无输出
/**
订阅1接收到: 初始值
订阅1接收到: 更新值1
订阅2接收到: 更新值1
当前值:更新值1
订阅2完成: finished
订阅1完成: finished
*/
实际应用场景
1. 状态管理
适合存储和传递应用状态(如用户信息、设置等):
class UserManager {
// 存储当前用户状态,初始为未登录
let currentUser = CurrentValueSubject<User?, Never>(nil)
func login(user: User) {
currentUser.send(user) // 更新当前用户
}
func logout() {
currentUser.send(nil) // 清除当前用户
}
}
struct User {
let id: String
let name: String
}
// 使用
let userManager = UserManager()
userManager.currentUser.sink { user in
if let user = user {
print("当前登录用户:\(user.name)")
} else {
print("未登录")
}
}
userManager.login(user: User(id: "1", name: "张三")) // 输出:当前登录用户:张三
2. 与 UI 绑定
在 UIKit/SwiftUI 中绑定界面元素:
// SwiftUI示例
class SettingsViewModel: ObservableObject {
let volume = CurrentValueSubject<Double, Never>(0.5)
func increaseVolume() {
let newValue = min(volume.value + 0.1, 1.0)
volume.send(newValue)
}
}
struct VolumeView: View {
@StateObject var viewModel = SettingsViewModel()
@State var currentVolume: Double = 0
var body: some View {
VStack {
Text("音量:\(currentVolume, specifier: "%.1f")")
Button("增加音量") {
viewModel.increaseVolume()
}
}
.onReceive(viewModel.volume) { newVolume in
currentVolume = newVolume
}
}
}
3. 数据缓存
临时存储最新数据,供新订阅者使用
class DataRepository {
private let latestData = CurrentValueSubject<DataModel?, Error>(nil)
private var cancellables = Set<AnyCancellable>()
func fetchData() {
// 模拟网络请求
URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map { $0.data }
.decode(type: DataModel.self, decoder: JSONDecoder())
.sink(
receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
self?.latestData.send(completion: .failure(error))
}
},
receiveValue: { [weak self] data in
self?.latestData.send(data) // 缓存最新数据
}
)
.store(in: &cancellables)
}
// 提供数据订阅接口
func dataPublisher() -> AnyPublisher<DataModel?, Error> {
return latestData.eraseToAnyPublisher()
}
}
4.3 PassthroughSubject
与 PassthroughSubject 的对比
特性 | CurrentValueSubject | PassthroughSubject |
初始值 | 需要提供 | 不需要 |
存储当前值 | 是 | 否 |
新订阅者 | 立即收到当前值 | 只收到订阅后的新值 |
访问当前值 | 通过 value 属性 | 无法直接访问 |