一、Combine 的核心概念
要理解 Combine,首先需要掌握几个核心概念,它们构成了 Combine 编程的基础:
- Publisher(发布者)
作用:负责产生和发送事件流(数据流或信号)。
特点:可以发送三种类型的事件:
.output(_ value):发送一个数据值(事件流中的正常数据)。
.failure(_ error):发送一个错误(事件流终止,后续不再发送数据)。
.finished:表示事件流正常结束(后续不再发送数据)。
常见示例:
NotificationCenter.Publisher:监听系统通知(如网络状态变化、键盘弹出)。
URLSession.DataTaskPublisher:发起网络请求并返回数据。
Just:发送单个数据后立即结束(用于包装静态数据)。
CurrentValueSubject/PassthroughSubject:手动发送事件的发布者(后续详细介绍)。 - Subscriber(订阅者)
作用:订阅发布者,接收并处理其发送的事件。
特点:必须实现 receive(subscription:)、receive(_ input:)、receive(completion:) 三个方法,分别用于处理 “订阅成功”、“接收数据”、“事件流结束(成功 / 失败)”。
常见示例:
sink:Combine 提供的便捷订阅者,通过闭包处理数据和结束事件(最常用)。
assign:将发布者的输出直接赋值给对象的属性(如 UI 控件的 text、isHidden 等)。 - Operator(运算符)
作用:对发布者发送的事件流进行处理(如过滤、转换、合并、延迟等),返回一个新的发布者。
特点:运算符可以链式调用,形成一个 “事件处理管道”,让代码逻辑清晰、可组合。
常见分类:
过滤类:filter(过滤不符合条件的数据)、removeDuplicates(去除重复数据)、compactMap(过滤 nil 并转换类型)。
转换类:map(将数据转换为另一种类型)、flatMap(将数据映射为新的发布者,并合并其事件流)、scan(累积数据并返回中间结果)。
合并类:merge(合并多个发布者的事件流)、zip(将多个发布者的事件按顺序配对组合)。
时间类:delay(延迟发送事件)、throttle(限制事件发送频率)、debounce(防抖,延迟一段时间后发送最后一个事件)。 - Subscription(订阅关系)
作用:连接发布者和订阅者的桥梁,用于管理订阅的生命周期(如取消订阅)。
特点:订阅者通过调用 publisher.subscribe(subscriber) 生成一个 Subscription 对象,订阅者可以通过该对象取消订阅(避免内存泄漏)。
自动管理:Combine 中订阅默认是 “自动取消” 的(当订阅者被释放时,订阅会自动取消),但手动管理订阅(如通过 AnyCancellable 存储订阅)是更安全的实践。 - Subject(主题)
作用:既是发布者(可发送事件),也是订阅者(可接收其他发布者的事件),用于手动控制事件流的发送。
常见类型:
PassthroughSubject:只发送 “订阅后” 接收到的事件(不存储历史数据)。
CurrentValueSubject:存储最新的一个数据值,当有新订阅者时,会立即发送该最新值(适用于需要 “状态保持” 的场景,如用户登录状态、购物车数据)。
二、Combine 的基本用法(代码示例)
下面通过几个简单的示例,展示 Combine 的核心用法:
示例 1:使用 Just 和 sink 处理静态数据
swift
import Combine
// 1. 创建发布者:Just 发送单个数据 "Hello, Combine!" 后结束
let publisher = Just("Hello, Combine!")
// 2. 订阅发布者:使用 sink 处理数据和结束事件
let cancellable = publisher
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("事件流正常结束")
case .failure(let error):
print("事件流失败:(error)")
}
},
receiveValue: { value in
print("接收到数据:(value)")
}
)
// 输出结果:
// 接收到数据:Hello, Combine!
// 事件流正常结束
示例 2:使用 CurrentValueSubject 管理状态
swift
import Combine
// 1. 创建 CurrentValueSubject:存储当前计数(初始值为 0)
let countSubject = CurrentValueSubject<Int, Never>(0)
// 2. 第一次订阅:会立即收到初始值 0
let cancellable1 = countSubject
.sink(
receiveCompletion: { _ in },
receiveValue: { value in
print("订阅者 1 收到计数:(value)")
}
)
// 3. 发送新事件
countSubject.send(1)
countSubject.send(2)
// 4. 第二次订阅:会立即收到最新值 2
let cancellable2 = countSubject
.sink(
receiveCompletion: { _ in },
receiveValue: { value in
print("订阅者 2 收到计数:(value)")
}
)
// 5. 发送结束事件
countSubject.send(completion: .finished)
// 输出结果:
// 订阅者 1 收到计数:0
// 订阅者 1 收到计数:1
// 订阅者 1 收到计数:2
// 订阅者 2 收到计数:2
示例 3:使用运算符链式处理事件流
swift
import Combine
// 1. 创建发布者:发送 1-10 的整数
let numbersPublisher = (1...10).publisher
// 2. 链式调用运算符:过滤偶数 → 乘以 2 → 取前 3 个数据
let cancellable = numbersPublisher
.filter { $0 % 2 == 0 } // 过滤偶数:2,4,6,8,10
.map { $0 * 2 } // 乘以 2:4,8,12,16,20
.prefix(3) // 取前 3 个:4,8,12
.sink(
receiveCompletion: { print("结束:($0)") },
receiveValue: { print("处理后的值:($0)") }
)
// 输出结果:
// 处理后的值:4
// 处理后的值:8
// 处理后的值:12
// 结束:finished
示例 4:网络请求(URLSession.DataTaskPublisher)
swift
import Combine
import Foundation
// 1. 定义网络请求的发布者:发起 GET 请求
guard let url = URL(string: "https://api.example.com/data") else { fatalError("无效 URL") }
let dataPublisher = URLSession.shared.dataTaskPublisher(for: url)
.tryMap { data, response in
// 转换:验证响应状态码(200-299),并返回数据
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw NSError(domain: "网络错误", code: -1, userInfo: nil)
}
return data
}
.decode(type: [User].self, decoder: JSONDecoder()) // 解码 JSON 数据为 [User] 数组
// 2. 订阅网络请求结果
let cancellable = dataPublisher
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("网络请求失败:(error)")
}
},
receiveValue: { users in
print("获取到 (users.count) 个用户")
}
)
// 注:User 需是 Decodable 协议的实现类,用于解析 JSON 数据
struct User: Decodable {
let id: Int
let name: String
}
三、Combine 的高级用法
- Subject 的手动控制
PassthroughSubject 和 CurrentValueSubject 是手动发送事件的核心工具,适用于 “响应 UI 交互”“状态变化通知” 等场景:
swift
// 场景:按钮点击事件 → 发送事件流
let buttonTapSubject = PassthroughSubject<Void, Never>()
// 模拟按钮点击(手动发送事件)
buttonTapSubject.send() // 第一次点击
buttonTapSubject.send() // 第二次点击
// 订阅按钮点击事件
let cancellable = buttonTapSubject
.sink { _ in
print("按钮被点击")
}
// 输出:
// 按钮被点击
// 按钮被点击
2. 合并多个发布者
使用 merge 或 zip 合并多个事件流,适用于 “多数据源协同” 场景(如同时获取用户信息和商品列表):
swift
// 示例:合并两个发布者的事件流
let publisher1 = Just("A").delay(for: 1, scheduler: RunLoop.main)
let publisher2 = Just("B").delay(for: 2, scheduler: RunLoop.main)
// merge:按事件发送顺序合并(先 A 后 B)
publisher1.merge(with: publisher2)
.sink { print("merge 结果:($0)") }
// zip:按索引配对合并((A,B))
publisher1.zip(publisher2)
.sink { print("zip 结果:($0)") }
// 输出:
// merge 结果:A
// merge 结果:B
// zip 结果:(A, B)
3. 处理错误
Combine 中错误会终止事件流,需通过 tryMap、catch、retry 等运算符处理错误:
swift
// 示例:网络请求失败后重试 2 次,失败后返回默认数据
let dataPublisher = URLSession.shared.dataTaskPublisher(for: url)
.tryMap { data, response in
// 验证响应状态码
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw NSError(domain: "网络错误", code: -1, userInfo: nil)
}
return data
}
.decode(type: [User].self, decoder: JSONDecoder())
.retry(2) // 失败后重试 2 次
.catch { error -> Just<[User]> in
// 捕获错误,返回默认数据
print("请求失败,返回默认数据:(error)")
return Just([User(id: 0, name: "默认用户")])
}
// 订阅结果
dataPublisher.sink { users in
print("最终数据:(users)")
}
4. 与 SwiftUI 结合
Combine 与 SwiftUI 深度集成,通过 @Published 属性包装器将对象属性转换为发布者,实现 “数据驱动 UI”:
swift
import SwiftUI
import Combine
// 定义数据模型(ObservableObject + @Published)
class UserViewModel: ObservableObject {
@Published var username: String = "" // @Published 自动生成发布者
@Published var isButtonEnabled: Bool = false
// 订阅 username 变化,控制按钮状态
private var cancellable: AnyCancellable?init() {// 监听 username 变化:长度 >=3 时启用按钮cancellable = $username.map { $0.count >= 3 }.assign(to: \.isButtonEnabled, on: self)
}
}
// SwiftUI 视图
struct UserView: View {
@ObservedObject var viewModel = UserViewModel()
var body: some View {VStack(spacing: 20) {TextField("输入用户名", text: $viewModel.username).textFieldStyle(.roundedBorder)Button("提交") {print("用户名:\(viewModel.username)")}.disabled(!viewModel.isButtonEnabled) // 绑定按钮状态}.padding()
}
}
四、Combine 的优势与适用场景
优势:
统一异步处理:将网络请求、通知、UI 交互等不同类型的异步事件,统一为 “发布者 - 订阅者” 模型,避免回调地狱(Callback Hell)。
高可组合性:通过运算符链式调用,将复杂逻辑拆分为独立的处理步骤,代码清晰、易维护。
响应式状态管理:CurrentValueSubject、@Published 等工具简化了状态变化的通知和管理,尤其适合多组件共享状态(如用户登录状态、全局配置)。
内存安全:通过 AnyCancellable 管理订阅生命周期,避免因 “订阅者未释放导致的内存泄漏”。
适用场景:
网络请求(数据获取、提交)。
状态管理(用户信息、购物车、设置项)。
UI 交互响应(按钮点击、文本输入、滑动手势)。
事件通知(系统通知、自定义事件)。
数据处理(过滤、转换、合并多数据源)。
五、注意事项
订阅生命周期管理:所有订阅必须通过 AnyCancellable 存储(如 var cancellables = Set
swift
// 正确用法:存储订阅
var cancellables = Set
Just("Hello")
.sink { print($0) }
.store(in: &cancellables) // 存储到 Set 中
避免循环引用:在 sink、assign 等闭包中引用 self 时,需使用 [weak self] 避免循环引用(尤其是在类中)。
swift
class MyClass {
var value: String = ""
var cancellables = Set
func setup() {Just("Test").sink { [weak self] newValue inself?.value = newValue // 弱引用 self,避免循环引用}.store(in: &cancellables)
}
}
错误处理:发布者发送 .failure 事件后会终止,需通过 catch、retry 等运算符处理错误,避免事件流意外中断。
线程调度:默认情况下,发布者的事件发送和订阅者的处理在同一线程(如网络请求在后台线程),如需切换到主线程(如更新 UI),需使用 receive(on:) 运算符。
swift
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: DispatchQueue.main) // 切换到主线程
.sink { [weak self] data in
self?.updateUI(with: data) // 主线程更新 UI
}
.store(in: &cancellables)
总结
Combine 是 iOS 开发中处理异步事件和状态管理的强大工具,其核心是 “发布者 - 订阅者 - 运算符” 模型。通过 Combine,你可以将复杂的异步逻辑拆分为清晰、可组合的步骤,同时简化状态管理和 UI 响应式更新。掌握 Combine 不仅能提升开发效率,还能让代码更健壮、易维护,是 iOS 进阶开发的必备技能之一。