好的,我们来详细讲解一下 Combine 框架的基本使用。Combine 是 Apple 在 2019 年推出的一个声明式的异步事件处理框架,它使用可组合的函数式转换来处理随时间变化的事件流。它是 SwiftUI 应用中管理状态和异步操作的核心。
核心概念
理解 Combine 的关键在于掌握三个核心概念:
-
Publisher(发布者) :事件的源头。它可以发射三种东西:
- 值(Values):即你所关心的数据。
- 完成(Completion):一个成功或失败的事件,表示流已结束。
- 失败(Failure):一个错误类型,表示流因错误而终止。
- 一个 Publisher 在没有被订阅前,什么都不会做。
-
Subscriber(订阅者) :事件的接收端。它向 Publisher 发起订阅,并接收来自 Publisher 的事件。常见的 Subscriber 有:
sink
: 接收值和完成事件。assign
: 将接收到的值直接设置到某个对象的属性上。
-
Operator(操作符) :处理事件的工具。它们本身也是 Publisher,接收上游的事件,进行处理后,再发送给下游。通过操作符,你可以过滤、转换、组合事件流。这是 Combine 强大和灵活的关键。
- 例如:
map
,filter
,flatMap
,merge
,zip
等。
- 例如:
数据流图 : Publisher
-> (经过零个或多个 Operator
) -> Subscriber
基本使用步骤
1. 创建 Publisher
有很多方式可以创建 Publisher:
-
来自 Foundation :许多现有的 Cocoa API 都提供了 Publisher 版本(通常在
...Publisher
属性中)。URLSession.dataTaskPublisher(for:)
: 网络请求。NotificationCenter.default.publisher(for:)
: 通知。Timer.publish(every:on:in:)
: 定时器。
-
使用内置方法 :
Just
: 发射一个值然后立即结束。Future
: 异步产生一个结果(成功或失败)。Empty
/Fail
/Deferred
: 用于特殊场景。
-
来自
@Published
属性包装器 (在 SwiftUI 中非常常见):swiftclass ViewModel { @Published var username: String = "" // $username 就是一个 Publisher<String, Never> }
2. 使用 Operator 进行转换(可选但常见)
在 Publisher 和 Subscriber 之间,你可以链式调用多个操作符。
swift
// 假设有一个发射字符串的 publisher
let myPublisher = Just("hello, world")
// 使用操作符转换
let transformedPublisher = myPublisher
.map { $0.uppercased() } // 转换为大写
.filter { !$0.isEmpty } // 过滤空字符串
3. 使用 Subscriber 进行订阅和接收
最终,你需要一个 Subscriber 来消费数据流。
使用 sink
订阅: sink
接收两个闭包:一个处理接收到的值,另一个处理完成事件。
swift
// 1. 创建一个永远不会失败的整数 Publisher
let publisher = [1, 2, 3, 4, 5].publisher
// 2. & 3. 使用操作符并订阅
let cancellable = publisher
.filter { $0 % 2 == 0 } // 操作符:只保留偶数
.map { $0 * $0 } // 操作符:平方
.sink(
receiveCompletion: { completion in
// 处理完成事件
switch completion {
case .finished:
print("流正常结束")
case .failure(let error):
print("流因错误结束: (error)")
}
},
receiveValue: { value in
// 处理接收到的每一个值
print("接收到的值: (value)")
}
)
// 输出:
// 接收到的值: 4
// 接收到的值: 16
// 流正常结束
使用 assign
订阅: assign
将接收到的值直接赋值给某个对象的某个属性。
swift
class MyClass {
var value: String = "" {
didSet {
print("value 被设置为: (value)")
}
}
}
let myObject = MyClass()
let publisher = Just("New Value")
// 将 publisher 发出的值赋值给 myObject 的 value 属性
let cancellable = publisher.assign(to: \.value, on: myObject)
// 输出:value 被设置为: New Value
关键点:Cancellable 和内存管理
- 当你调用
sink
或assign
时,返回值是一个AnyCancellable
对象。 - 你必须强引用这个对象 。当这个
AnyCancellable
对象被释放(比如离开作用域)时,它会自动取消订阅并释放资源。 - 如果你不保留它,订阅会立即被取消,你可能什么也收不到。
- 通常的做法是将所有的
AnyCancellable
收集到一个 `Set`` 中。
swift
import Combine
class MyViewModel {
@Published var currentTime: String = ""
private var cancellables = Set<AnyCancellable>() // 用于存储订阅
func setupTimer() {
// 创建一个每秒发射一次时间的 Publisher
Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.map { date in
// 操作符:将 Date 转换为字符串
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter.string(from: date)
}
.sink { [weak self] newTime in
// 订阅:将新时间赋值给属性
self?.currentTime = newTime
}
.store(in: &cancellables) // 至关重要!将订阅存储到 Set 中
}
}
常用操作符示例
-
map
: 转换值。swift.map { value in value * 2 }
-
filter
: 过滤值。swift.filter { value in value > 10 }
-
flatMap
: 将多个 Publisher 扁平化为一个。swift// 假设有一个函数 func fetch(id: Int) -> Publisher<String, Error> .flatMap { id in fetch(id: id) }
-
catch
: 错误处理,从错误中恢复。swift.catch { error in Just("Default Value") } // 出错时返回一个默认值
-
combineLatest
: 组合多个 Publisher,当任何一个发射新值时,发送所有 Publisher 的最新值的元组。 -
merge
: 将多个 Publisher 的事件流合并为一个。 -
debounce
: 防抖,比如用于搜索框,等待用户停止输入后再发起请求。swift.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
总结
- 找到源头 : 确定你的数据来自哪个
Publisher
(通知、网络请求、定时器、@Published
属性等)。 - 组装管道 : 使用
Operator
链式地转换、过滤、组合数据流。 - 消费结果 : 在末端使用
sink
或assign
来订阅和处理最终的数据或错误。 - 管理生命周期 : 务必存储返回的
AnyCancellable
到你的Set<AnyCancellable>
中,以避免订阅被意外取消。
Combine 的学习曲线稍陡,但一旦掌握,它将成为你处理所有异步和数据流问题的强大而优雅的工具,尤其是在 SwiftUI 开发中。