Search Field With Suggestions
A search field that lists matching suggestions as the user types and fills the field when one is chosen.

- Inputs
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- search · searchable · suggestions · autocomplete · completion · filter · búsqueda · buscador · sugerencias · autocompletar · filtro
Description
Section titled “Description”Needs a navigation container, such as NavigationStack, to show the field.
struct SearchFieldWithSuggestionsDemo: View { private static let cities = ["Madrid", "Málaga", "Marseille", "Melbourne", "Lisbon", "London", "Seoul", "Oslo", "Kyoto"]
@State private var text = "" @FocusState private var isSearching: Bool
var body: some View { NavigationStack { VStack(spacing: 12) { Image(systemName: "globe.europe.africa.fill") .font(.largeTitle) .foregroundStyle(.teal.gradient) .accessibilityHidden(true) Text(text.isEmpty ? "Where to?" : "Results for \(text)") .font(.title2.bold()) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(LinearGradient(colors: [.teal.opacity(0.14), .blue.opacity(0.08)], startPoint: .top, endPoint: .bottom)) .navigationTitle("Cities") .searchFieldWithSuggestions( text: $text, suggestions: Self.cities, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search cities" ) .searchFocused($isSearching) .task { // The field must be on screen before it can take focus. try? await Task.sleep(for: .seconds(0.4)) guard !Task.isCancelled else { return } isSearching = true } } }}API reference
Section titled “API reference”searchFieldWithSuggestions(text:suggestions:placement:prompt:limit:)
| Parameter | Type | Default |
|---|---|---|
text | Binding<String> | Required |
suggestions | [String] | Required |
placement | SearchFieldPlacement | .automatic |
prompt | String | "Search" |
limit | Int | 6 |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// Picks the suggestions to show for what has been typed so far.enum SearchFieldWithSuggestionsFilter { /// Ignores case and accents, lists prefix matches first and drops duplicates. /// Returns nothing for an empty query, or once the text is already a full suggestion. static func matches(for query: String, in suggestions: [String], limit: Int = 6) -> [String] { let needle = normalized(query) guard !needle.isEmpty, limit > 0 else { return [] } guard !suggestions.contains(where: { normalized($0) == needle }) else { return [] }
var seen = Set<String>() let unique = suggestions.filter { seen.insert(normalized($0)).inserted } let prefix = unique.filter { normalized($0).hasPrefix(needle) } let inside = unique.filter { normalized($0).contains(needle) && !normalized($0).hasPrefix(needle) } return Array((prefix + inside).prefix(limit)) }
private static func normalized(_ text: String) -> String { text.trimmingCharacters(in: .whitespacesAndNewlines) .folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil) }}
/// Lists matching suggestions under a search field as the person types./// Choosing one fills the field.struct SearchFieldWithSuggestions: ViewModifier { @Binding var text: String let suggestions: [String] var placement: SearchFieldPlacement = .automatic var prompt = "Search" var limit = 6
func body(content: Content) -> some View { content .searchable(text: $text, placement: placement, prompt: prompt) .searchSuggestions { ForEach(SearchFieldWithSuggestionsFilter.matches(for: text, in: suggestions, limit: limit), id: \.self) { suggestion in Label(suggestion, systemImage: "magnifyingglass") .searchCompletion(suggestion) } } }}
extension View { /// Needs a navigation container, such as `NavigationStack`, to show the field. func searchFieldWithSuggestions( text: Binding<String>, suggestions: [String], placement: SearchFieldPlacement = .automatic, prompt: String = "Search", limit: Int = 6 ) -> some View { modifier(SearchFieldWithSuggestions(text: text, suggestions: suggestions, placement: placement, prompt: prompt, limit: limit)) }}
// MARK: - Demo
struct SearchFieldWithSuggestionsDemo: View { private static let cities = ["Madrid", "Málaga", "Marseille", "Melbourne", "Lisbon", "London", "Seoul", "Oslo", "Kyoto"]
@State private var text = "" @FocusState private var isSearching: Bool
var body: some View { NavigationStack { VStack(spacing: 12) { Image(systemName: "globe.europe.africa.fill") .font(.largeTitle) .foregroundStyle(.teal.gradient) .accessibilityHidden(true) Text(text.isEmpty ? "Where to?" : "Results for \(text)") .font(.title2.bold()) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(LinearGradient(colors: [.teal.opacity(0.14), .blue.opacity(0.08)], startPoint: .top, endPoint: .bottom)) .navigationTitle("Cities") .searchFieldWithSuggestions( text: $text, suggestions: Self.cities, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search cities" ) .searchFocused($isSearching) .task { // The field must be on screen before it can take focus. try? await Task.sleep(for: .seconds(0.4)) guard !Task.isCancelled else { return } isSearching = true } } }}
#Preview { SearchFieldWithSuggestionsDemo()}