```html SwiftUI: How to Search Bar (iOS 17+, 2026)

How to implement a search bar in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: searchable Updated: May 11, 2026
TL;DR

Attach .searchable(text: $searchText) to a List inside a NavigationStack, then filter your data with a computed property — iOS handles the rest, including keyboard dismissal and placement.

struct ContentView: View {
    @State private var searchText = ""
    let fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]

    var filtered: [String] {
        searchText.isEmpty ? fruits : fruits.filter {
            $0.localizedCaseInsensitiveContains(searchText)
        }
    }

    var body: some View {
        NavigationStack {
            List(filtered, id: \.self) { Text($0) }
                .navigationTitle("Fruits")
                .searchable(text: $searchText, prompt: "Search fruits")
        }
    }
}

Full implementation

The example below models a searchable contact list with a struct data source, a computed filteredContacts property, and an empty-state view so the UI stays polished when nothing matches. The .searchable modifier is placed on the List — not the NavigationStack — so iOS automatically positions the bar below the navigation title and collapses it on scroll.

import SwiftUI

struct Contact: Identifiable {
    let id = UUID()
    let name: String
    let role: String
}

struct ContactListView: View {
    @State private var searchText = ""

    private let contacts: [Contact] = [
        Contact(name: "Ada Lovelace",     role: "Engineer"),
        Contact(name: "Alan Turing",      role: "Mathematician"),
        Contact(name: "Grace Hopper",     role: "Admiral"),
        Contact(name: "Tim Berners-Lee",  role: "Inventor"),
        Contact(name: "Linus Torvalds",   role: "Engineer"),
        Contact(name: "Margaret Hamilton",role: "Scientist"),
    ]

    private var filteredContacts: [Contact] {
        guard !searchText.isEmpty else { return contacts }
        return contacts.filter {
            $0.name.localizedCaseInsensitiveContains(searchText) ||
            $0.role.localizedCaseInsensitiveContains(searchText)
        }
    }

    var body: some View {
        NavigationStack {
            Group {
                if filteredContacts.isEmpty {
                    ContentUnavailableView.search(text: searchText)
                } else {
                    List(filteredContacts) { contact in
                        VStack(alignment: .leading, spacing: 2) {
                            Text(contact.name)
                                .font(.headline)
                            Text(contact.role)
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }
                        .accessibilityElement(children: .combine)
                        .accessibilityLabel("\(contact.name), \(contact.role)")
                    }
                }
            }
            .navigationTitle("Contacts")
            .searchable(
                text: $searchText,
                placement: .navigationBarDrawer(displayMode: .always),
                prompt: "Name or role"
            )
        }
    }
}

#Preview {
    ContactListView()
}

How it works

  1. .searchable(text:placement:prompt:) — Attaching this modifier to the List tells SwiftUI to inject a native UISearchController into the hosting navigation bar. The placement: .navigationBarDrawer(displayMode: .always) parameter keeps the bar permanently visible rather than hiding it when the list scrolls up.
  2. filteredContacts computed property — Because it depends on @State var searchText, SwiftUI automatically re-evaluates and re-renders the list every time the user types a character. The short-circuit guard !searchText.isEmpty avoids iterating the full array when the query is blank.
  3. ContentUnavailableView.search(text:) — Introduced in iOS 17, this ready-made empty-state view renders the system magnifying-glass illustration with your query highlighted, replacing the blank list when filteredContacts.isEmpty.
  4. localizedCaseInsensitiveContains — This Foundation method respects the device locale and diacritic rules (e.g. "e" matches "é") without any extra configuration, making search feel natural across languages.
  5. accessibilityElement(children: .combine) — Combining the name and role labels into a single VoiceOver element reads them as one phrase instead of two separate taps, giving a cleaner screen-reader experience.

Variants

Search scopes (filter by category)

Add .searchScopes to render a segmented control beneath the search bar, letting users restrict results to a category without extra UI.

enum RoleScope: String, CaseIterable {
    case all = "All"
    case engineer = "Engineer"
    case other = "Other"
}

struct ScopedSearchView: View {
    @State private var searchText = ""
    @State private var selectedScope = RoleScope.all

    private let contacts: [Contact] = [
        Contact(name: "Ada Lovelace",     role: "Engineer"),
        Contact(name: "Grace Hopper",     role: "Admiral"),
        Contact(name: "Linus Torvalds",   role: "Engineer"),
    ]

    private var filteredContacts: [Contact] {
        contacts.filter { contact in
            let matchesScope: Bool = switch selectedScope {
            case .all:      true
            case .engineer: contact.role == "Engineer"
            case .other:    contact.role != "Engineer"
            }
            let matchesQuery = searchText.isEmpty ||
                contact.name.localizedCaseInsensitiveContains(searchText)
            return matchesScope && matchesQuery
        }
    }

    var body: some View {
        NavigationStack {
            List(filteredContacts) { contact in
                Text(contact.name)
            }
            .navigationTitle("Contacts")
            .searchable(text: $searchText, prompt: "Search")
            .searchScopes($selectedScope) {
                ForEach(RoleScope.allCases, id: \.self) { scope in
                    Text(scope.rawValue).tag(scope)
                }
            }
        }
    }
}

Dismissing search programmatically

Inject @Environment(\.dismissSearch) private var dismissSearch into any child view, then call dismissSearch() on a button tap or after a navigation push. This closes the keyboard and resets the search bar without requiring you to manually clear searchText.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a search bar in SwiftUI for iOS 17+.
Use .searchable with placement .navigationBarDrawer(displayMode: .always).
Filter a list of model objects using a computed property.
Show ContentUnavailableView.search(text:) when results are empty.
Make it accessible (VoiceOver labels on each row).
Add a #Preview with realistic sample data.

Drop this prompt into Soarias during the Build phase to scaffold a fully wired search feature with empty-state handling and accessibility in a single pass — no boilerplate hunting required.

Related

FAQ

Does this work on iOS 16?

The .searchable modifier itself is available from iOS 15+. However, ContentUnavailableView and the .search(text:) static factory require iOS 17+. Wrap those with if #available(iOS 17, *) { ... } else { /* custom empty view */ } to maintain iOS 16 compatibility.

How do I search across multiple fields or nested objects?

Extend your filter predicate with || chains or implement a var searchTokens: String computed property on your model that concatenates all searchable text into one string, then run a single localizedCaseInsensitiveContains against it. For full-text search across thousands of records, consider using NSPredicate with a SwiftData #Predicate macro so the filtering runs in the persistent store rather than in memory.

What's the UIKit equivalent?

In UIKit you create a UISearchController, assign it to navigationItem.searchController, and implement UISearchResultsUpdating to receive query changes. SwiftUI's .searchable modifier wraps exactly this machinery, so the two are functionally equivalent but the SwiftUI version requires roughly 80% less code.

Last reviewed: 2026-05-11 by the Soarias team.

```