SwiftUI Patterns

Search & Filter with Debouncing

May 19, 2026
5 min read
Featured image for blog post: Search & Filter with Debouncing

Searching without debouncing fires API calls on every keystroke. Here's how to implement efficient search with task cancellation.

Follow along with the code: iOS-Practice on GitHub

The Problem

Without debouncing, typing "swift" fires 5 API calls:

  • "s" → API call
  • "sw" → API call
  • "swi" → API call
  • "swif" → API call
  • "swift" → API call

This wastes resources and can cause race conditions.

The Solution

struct SearchFilterExerciseView: View {
    @State private var products: [Product] = []
    @State private var searchText = ""
    @State private var isLoading = false
    @State private var searchTask: Task<Void, Never>?

    var body: some View {
        List {
            // ... content
        }
        .searchable(text: $searchText, prompt: "Search products...")
        .onChange(of: searchText) { _, newValue in
            performSearch(query: newValue)
        }
        .task {
            await loadProducts()
        }
    }

    private func performSearch(query: String) {
        // Cancel previous search
        searchTask?.cancel()

        guard !query.isEmpty else {
            Task { await loadProducts() }
            return
        }

        searchTask = Task {
            // Debounce: wait 300ms before searching
            try? await Task.sleep(nanoseconds: 300_000_000)

            // Check if cancelled during wait
            guard !Task.isCancelled else { return }

            isLoading = true
            do {
                products = try await MockAPIService.shared.searchProducts(query: query)
            } catch {
                if !Task.isCancelled {
                    // Handle error
                }
            }
            isLoading = false
        }
    }
}

Key Components

1. Track the Search Task

@State private var searchTask: Task<Void, Never>?

2. Cancel on New Input

searchTask?.cancel()

3. Debounce with Sleep

try? await Task.sleep(nanoseconds: 300_000_000) // 300ms

4. Check Cancellation

guard !Task.isCancelled else { return }

The .searchable Modifier

SwiftUI provides the search bar:

.searchable(text: $searchText, prompt: "Search products...")

This adds:

  • Search bar in navigation
  • Keyboard with search button
  • Cancel button
  • Automatic hiding when scrolling

Responding to Changes

Use .onChange to react to search text:

.onChange(of: searchText) { _, newValue in
    performSearch(query: newValue)
}

Empty Search Handling

Clear search → reload all products:

guard !query.isEmpty else {
    Task { await loadProducts() }
    return
}

Error Handling

Only handle errors if not cancelled:

} catch {
    if !Task.isCancelled {
        self.error = error
    }
}

Cancellation throws CancellationError, which we can ignore.

Interview Tip

This pattern demonstrates understanding of:

  • Task cancellation (cooperative)
  • Debouncing (300ms is standard)
  • Race condition prevention
  • Resource efficiency

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch