SwiftUI
Search Bar
iOS Development
User Interface
Swift Programming

How to display a search bar with SwiftUI

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Adding a search bar in SwiftUI is easy for a demo, but production screens usually need more than one modifier. A good implementation handles local filtering, remote queries, empty states, and predictable behavior when users clear or edit text quickly. The feature feels simple only when the state model underneath it is disciplined.

Build a Basic Searchable List

On iOS 15 and newer, the preferred API is searchable. You bind a query string and filter your data source from that value.

swift
1import SwiftUI
2
3struct Product: Identifiable {
4    let id = UUID()
5    let name: String
6    let category: String
7}
8
9struct ProductListView: View {
10    @State private var query = ""
11
12    private let products: [Product] = [
13        Product(name: "Apple", category: "Fruit"),
14        Product(name: "Banana", category: "Fruit"),
15        Product(name: "Bread", category: "Bakery"),
16        Product(name: "Cheese", category: "Dairy"),
17        Product(name: "Yogurt", category: "Dairy")
18    ]
19
20    private var filteredProducts: [Product] {
21        let text = query.trimmingCharacters(in: .whitespacesAndNewlines)
22        guard !text.isEmpty else { return products }
23
24        return products.filter {
25            $0.name.localizedCaseInsensitiveContains(text) ||
26            $0.category.localizedCaseInsensitiveContains(text)
27        }
28    }
29
30    var body: some View {
31        NavigationStack {
32            List(filteredProducts) { item in
33                VStack(alignment: .leading, spacing: 4) {
34                    Text(item.name).font(.headline)
35                    Text(item.category).font(.caption).foregroundStyle(.secondary)
36                }
37            }
38            .navigationTitle("Products")
39        }
40        .searchable(text: $query, prompt: "Search products")
41    }
42}

This pattern works well when the data set is already in memory. For many screens, it should be the first version you ship before adding remote search complexity.

Add Suggestions and Search Scopes

Suggestions reduce typing effort, and scopes narrow results when one query can map to different domains.

swift
1struct SearchWithScopeView: View {
2    enum Scope: String, CaseIterable, Identifiable {
3        case all = "All"
4        case fruit = "Fruit"
5        case dairy = "Dairy"
6
7        var id: String { rawValue }
8    }
9
10    @State private var query = ""
11    @State private var selectedScope: Scope = .all
12
13    private let suggestions = ["Apple", "Banana", "Cheese", "Yogurt"]
14
15    var body: some View {
16        List {
17            Text("Render filtered results here")
18        }
19        .searchable(text: $query, prompt: "Find items") {
20            ForEach(suggestions, id: \.self) { word in
21                Text(word).searchCompletion(word)
22            }
23        }
24        .searchScopes($selectedScope) {
25            ForEach(Scope.allCases) { scope in
26                Text(scope.rawValue).tag(scope)
27            }
28        }
29    }
30}

Keep suggestions relevant to frequent searches. Too many static completions can create noise and make the feature feel slower, even when performance is fine.

Support Remote Search Without Request Storms

When results come from an API, firing a request for every key press is expensive and can produce out of order responses. Use a debounce delay and cancel older tasks.

swift
1import Combine
2import Foundation
3
4@MainActor
5final class ProductSearchViewModel: ObservableObject {
6    @Published var query = ""
7    @Published private(set) var results: [String] = []
8    @Published private(set) var isLoading = false
9
10    private var cancellables = Set<AnyCancellable>()
11    private var currentTask: Task<Void, Never>?
12
13    init() {
14        $query
15            .removeDuplicates()
16            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
17            .sink { [weak self] text in
18                self?.search(text: text)
19            }
20            .store(in: &cancellables)
21    }
22
23    private func search(text: String) {
24        currentTask?.cancel()
25
26        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
27        guard !trimmed.isEmpty else {
28            results = []
29            return
30        }
31
32        currentTask = Task {
33            isLoading = true
34            defer { isLoading = false }
35
36            do {
37                try await Task.sleep(nanoseconds: 200_000_000)
38                try Task.checkCancellation()
39
40                let source = ["apple pie", "banana bread", "greek yogurt", "aged cheese"]
41                results = source.filter { $0.localizedCaseInsensitiveContains(trimmed) }
42            } catch {
43                results = []
44            }
45        }
46    }
47}

This prevents stale responses from overwriting newer queries and keeps the UI stable.

Decide Empty, Loading, and No Result States Early

A search feature feels polished when states are explicit:

  • Empty query shows default content or recent searches.
  • Loading state shows quick feedback, often a compact ProgressView.
  • No result state explains what happened and suggests next action.

If these states are not planned, teams often patch them late and create inconsistent navigation behavior.

Common Pitfalls

  • Filtering large arrays on the main thread without optimization, which causes typing lag.
  • Ignoring cancellation in remote search, allowing old responses to replace new ones.
  • Treating empty query as an error state instead of a valid browsing state.
  • Storing search text globally when only one screen needs it, creating state leaks.
  • Adding search scopes that duplicate each other and confuse users.

Summary

  • Use searchable as the default SwiftUI entry point for search UX.
  • Start with local filtering, then add suggestions and scopes only when they provide clear value.
  • Debounce remote queries and cancel stale work to avoid race conditions.
  • Define empty, loading, and no result states as part of the first design.
  • Keep search state close to the screen owner for simpler maintenance and testing.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.