Skip to main content

Command Palette

Search for a command to run...

Debounce in Swift: Ditch Combine for This One Simple Loop

Published
4 min read
Debounce in Swift: Ditch Combine for This One Simple Loop

Remember when you first learned about debouncing in iOS? You probably reached for Combine, set up a publisher chain with debounce(), and felt like you'd unlocked some secret knowledge. But what if I told you there's a cleaner, more Swift-y way to do it?

Enter Swift Async Algorithms – Apple's answer to making async operations feel as natural as a for loop.

What is Swift Async Algorithms?

Swift Async Algorithms is Apple's open-source library that brings familiar collection algorithms to the world of async sequences. Think of it as the missing standard library for async/await – it provides tools like debounce, throttle, merge, combineLatest, and more, but designed from the ground up for Swift's modern concurrency model.

Released in 2022, it's Apple's way of saying: "You don't need Combine for everything anymore."

The one-line summary: It's like the Combine framework, but built for async/await instead of publishers and subscribers.

Why Should You Care?

Let me paint a picture: You're building a search bar. Every time the user types, you want to search an API, but you don't want to spam it with requests. You need debouncing.

The old way (Combine):

  • Import a whole framework

  • Deal with publishers, subscribers, and cancellables

  • Store subscription lifecycles

  • Convert between async and publisher worlds

The new way (Async Algorithms):

  • Use native Swift concurrency

  • Write code that reads like English

  • No subscription management

  • Works seamlessly with async/await

Plus, if you're already using async/await in your app, why add Combine just for one operator?

The Real-World Example: Search Bar Debounce

Let's build the same search feature in both approaches. You'll instantly see why Async Algorithms feels like a breath of fresh air.

The Combine Way

import Combine
import UIKit

class CombineSearchViewController: UIViewController {
    @IBOutlet weak var searchBar: UISearchBar!
    @IBOutlet weak var tableView: UITableView!

    private var cancellables = Set<AnyCancellable>()
    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)
    }
}

What's happening here:

  1. Create a PassthroughSubject to emit search text

  2. Chain operators: debounceremoveDuplicatessink

  3. Bridge to async with Task inside sink

  4. Manage cancellables for cleanup

  5. Deal with two different concurrency models

The Async Algorithms Way

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)
        }
    }
}

The difference is night and day:

  1. Create an AsyncChannel (think of it as an async stream)

  2. Use a simple for await loop

  3. Chain operations right on the sequence: .debounce().removeDuplicates()

  4. Everything is async – no bridging needed

  5. No subscription management, no cancellables

Breaking Down the Async Algorithms Approach

Let's zoom in on the magic line:

for await query in searchStream.debounce(for: .milliseconds(500)).removeDuplicates() {
    await performSearch(query: query)
}

Here's what's happening:

  1. AsyncChannel: A sendable async sequence. Think of it as a pipeline where you send values from one end and receive them from the other.

  2. .debounce(for: .milliseconds(500)): Waits 500ms of "quiet time" before emitting the latest value. If new values arrive during that window, the timer resets.

  3. .removeDuplicates(): Filters out consecutive identical values. If the user types "cat", deletes it, then types "cat" again, we only search once.

  4. for await: The loop suspends at each iteration, waiting for the next value. Clean, sequential, readable.

Installation

Add Swift Async Algorithms to your project via Swift Package Manager:

https://github.com/apple/swift-async-algorithms

In Xcode: File → Add Package Dependencies → paste the URL above.

For more details visit


Want to dive deeper? Check out Apple's official documentation:

More from this blog

A

ArshTechPro

43 posts

A mobile expert primarily focused on the iOS ecosystem. Passionate about building robust, user-centric apps and exploring the latest in Swift, UIKit and SwiftUI.