Swift 中的 Debounce:抛弃 Combine,使用这一个简单循环
I’m happy to translate the article for you, but I need the full text of the post (the paragraphs, headings, and any explanatory content) in order to do so. The source link you provided is just a reference; I can’t retrieve the article’s contents directly.
Could you please paste the article’s text here (excluding the code blocks and URLs, which should remain unchanged)? Once I have the content, I’ll translate it into Simplified Chinese while preserving the original formatting and markdown.
Source:
进入 Swift Async Algorithms – Apple 为让异步操作像 for 循环一样自然的答案
什么是 Swift Async Algorithms?
Swift Async Algorithms 是 Apple 开源的库,它将熟悉的集合算法引入异步序列的世界。可以把它看作是 async/await 缺失的标准库支持——它提供了 debounce、throttle、merge、combineLatest 等工具,但从头开始为 Swift 的现代并发模型而设计。
- 于 2022 年发布,这是 Apple 表示:“你不再需要用 Combine 来处理所有事情”的方式。
- 一句话概括: 它像 Combine 框架,但是为 async/await 而非发布者和订阅者构建的。
为什么你需要在意?
想象一下你在构建一个搜索栏。每次用户输入时,你想查询 API,但又不想对它进行请求轰炸。你需要去抖动(debounce)。
| 旧方式(Combine) | 新方式(Async Algorithms) |
|---|---|
| 导入整个框架 | 使用原生 Swift 并发 |
| 处理发布者、订阅者和可取消对象 | 编写像英文一样的代码 |
| 管理订阅生命周期 | 无需订阅管理 |
| 在 async 与发布者世界之间转换 | 与 async/await 无缝协作 |
如果你的应用已经在使用 async/await,为什么还要为了一个操作符而额外引入 Combine 呢?
Source: …
真实案例:搜索栏防抖
下面展示了使用两种方式实现同一功能的代码。对比可以让你清晰地看到 Async Algorithms 的优势。
Combine 方式
import Combine
import UIKit
class CombineSearchViewController: UIViewController {
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var tableView: UITableView!
private var cancellables = Set()
private let searchSubject = PassthroughSubject<String, Never>()
private var results: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupSearch()
}
private func setupSearch() {
// Set up the debounce pipeline
searchSubject
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] query in
Task {
await self?.performSearch(query: query)
}
}
.store(in: &cancellables)
searchBar.delegate = self
}
private func performSearch(query: String) async {
// Simulate API call
try? await Task.sleep(nanoseconds: 500_000_000)
let mockResults = [
"Result for \(query) - 1",
"Result for \(query) - 2",
"Result for \(query) - 3"
]
await MainActor.run {
self.results = mockResults
self.tableView.reloadData()
}
}
}
extension CombineSearchViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchSubject.send(searchText)
}
}
发生了什么
PassthroughSubject会发出搜索文本的值。- 操作符被链式调用:
debounce → removeDuplicates → sink。 - 在
sink中我们使用Task桥接到 async。 - 必须管理一个
Set来进行清理。 - 两种并发模型(Combine + async/await)共存。
Async Algorithms 方式
import AsyncAlgorithms
import UIKit
class AsyncSearchViewController: UIViewController {
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var tableView: UITableView!
private let searchStream = AsyncChannel<String>()
private var results: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
Task {
await observeSearches()
}
}
private func observeSearches() async {
// This reads like plain English!
for await query in searchStream
.debounce(for: .milliseconds(500))
.removeDuplicates() {
await performSearch(query: query)
}
}
private func performSearch(query: String) async {
// Simulate API call
try? await Task.sleep(nanoseconds: 500_000_000)
let mockResults = [
"Result for \(query) - 1",
"Result for \(query) - 2",
"Result for \(query) - 3"
]
await MainActor.run {
self.results = mockResults
self.tableView.reloadData()
}
}
}
extension AsyncSearchViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
Task {
await searchStream.send(searchText)
}
}
}
为何有天壤之别
AsyncChannel充当一个可以send的 async 流。- 单个
for await循环消费防抖且去重后的值。 - 所有代码都保持在 async/await 世界中——无需桥接。
- 不需要 cancellables,也不需要额外的生命周期管理。
拆解 Async Algorithms 方法
魔法代码行:
for await query in searchStream
.debounce(for: .milliseconds(500))
.removeDuplicates() {
await performSearch(query: query)
}
正在发生什么?
| 组件 | 描述 |
|---|---|
AsyncChannel | 一个可发送的异步序列。可以把它想象成一个管道,你在一端推送值,在另一端拉取它们。 |
.debounce(for: .milliseconds(500)) | 在发出最新值之前等待 500 毫秒的“安静时间”。如果在计时器触发前有新值到达,计时器会重置。 |
.removeDuplicates() | 抑制连续相同的查询,防止不必要的 API 调用。 |
for await | 逐个消费异步序列的元素,就像普通的 for‑in 循环。 |
await performSearch(query:) | 直接调用异步搜索函数——不需要 Task {} 包装器。 |
所有这些都运行在 Swift 并发运行时上,为你提供确定性的取消、结构化并发,以及像散文一样的语法。
TL;DR
- Swift Async Algorithms 为异步序列提供集合式运算符(
debounce、throttle、merge,……)。 - 它让你可以用简洁的
async/await代码取代 Combine 风格的管道。 - 无需额外框架、无需 cancellable、无需桥接——纯原生 Swift 并发。
下次在 async‑first 代码库中需要防抖、节流或其他流处理逻辑时,试试看吧。你的未来的自己(以及代码审查者)会感谢你的。
Debounce – 在流中出现暂停后发出最新的值:
for await value in stream.debounce(for: .seconds(0.5), clock: .continuous) {
print(value) // 仅在用户停止输入 0.5 秒后触发
}
如果用户在该时间窗口内持续输入,计时器会重新计时。
.removeDuplicates()
过滤掉连续的相同值。
如果用户输入 “cat”,随后删除,再次输入 “cat”,搜索只会执行一次。
for await
循环在每次迭代时挂起,等待下一个值。
简洁、顺序化、易读。
安装
通过 Swift 包管理器将 Swift Async Algorithms 添加到你的项目中:
https://github.com/apple/swift-async-algorithms
在 Xcode 中:File → Add Package Dependencies → 粘贴上述 URL。
何时使用哪种?
使用 Async Algorithms 时:
- 你已经在使用
async/await - 从头开始构建新功能
- 想要更简洁、更易读的代码
- 你的团队对现代 Swift 并发感到舒适
使用 Combine 时:
- 处理已经使用 Combine 的遗留代码
- 需要紧密的 SwiftUI 集成(Combine 在这方面仍有优势)
- 构建包含众多操作符的复杂响应式管道
- 目标 iOS 版本早于
async/await
记忆技巧
- Combine = 发布者 push 值 → 你 subscribe 并 react
- Async Algorithms = 你 pull 值 → 你 await 并 process
再说一点:其他酷炫的运算符
Throttle – 在每个时间窗口内最多发出一个值
for await value in stream.throttle(for: .seconds(1), clock: .continuous) {
print(value)
}
Merge – 合并多个异步序列
for await value in merge(streamA, streamB) {
print(value)
}
Zip – 将多个序列的值成对组合
for await (a, b) in zip(streamA, streamB) {
print("Pair: \(a), \(b)")
}
CombineLatest – 获取多个流中的最新值
for await (a, b) in combineLatest(streamA, streamB) {
print("Latest: \(a), \(b)")
}
结论
Swift Async Algorithms 并没有取代 Combine——它为你提供了选择。
如果你已经全面采用了 async/await(而且你应该这么做),那么 Async Algorithms 是自然的下一步。
搜索栏示例完美地展示了这一点:在 Combine 中需要多个对象、订阅以及繁琐的思考,而现在只需一个可读的 for await 循环。
下次仅仅为了对文本字段进行去抖动而使用 Combine 时,尝试改用 Async Algorithms 吧。