Swift 中的 Debounce:抛弃 Combine,使用这一个简单循环

发布: (2026年1月20日 GMT+8 11:22)
9 min read
原文: Dev.to

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 缺失的标准库支持——它提供了 debouncethrottlemergecombineLatest 等工具,但从头开始为 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 为异步序列提供集合式运算符(debouncethrottlemerge,……)。
  • 它让你可以用简洁的 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 值 → 你 subscribereact
  • Async Algorithms = 你 pull 值 → 你 awaitprocess

再说一点:其他酷炫的运算符

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 吧。

想深入了解吗?

Back to Blog

相关文章

阅读更多 »

SC #11:任务组

TaskGroup 一个 TaskGroup 包含动态创建的子任务,可以串行或并发执行。只有当组完成时才被视为结束。

SC #8:取消 Task

Swift 和 SwiftUI 中的 Task 取消 > 注意:在 Swift 中,取消 Task 并不保证执行会立即停止。每个 Task 必须检查更多…

SC #10:解耦任务

一个脱耦的 Detached Task 以异步方式执行操作,脱离了包裹它的结构化并发上下文。不要继承这个 c...