SwiftUI Search API Reference
Overview
SwiftUI search is environment-based and navigation-consumed. You attach .searchable() to a view, but a navigation container (NavigationStack, NavigationSplitView, or TabView) renders the actual search field. This indirection is the source of most search bugs.
API Evolution
| iOS |
Key Additions |
| 15 |
.searchable(text:), isSearching, dismissSearch, suggestions, .searchCompletion(), onSubmit(of: .search) |
| 16 |
Search scopes (.searchScopes), search tokens (.searchable(text:tokens:)), SearchScopeActivation |
| 16.4 |
Search scope activation parameter (.onTextEntry, .onSearchPresentation) |
| 17 |
isPresented parameter, suggestedTokens parameter |
| 17.1 |
.searchPresentationToolbarBehavior(.avoidHidingContent) |
| 18 |
.searchFocused($isFocused) for programmatic focus control |
| 26 |
Bottom-aligned search, .searchToolbarBehavior(.minimize), Tab(role: .search), DefaultToolbarItem(kind: .search) β see axiom-swiftui-26-ref |
When to Use This Skill
- Adding search to a SwiftUI list or collection
- Implementing filter-as-you-type or submit-based search
- Adding search suggestions with auto-completion
- Using search scopes to narrow results by category
- Using search tokens for structured queries
- Controlling search focus programmatically
- Debugging "search field doesn't appear" issues
For iOS 26 search features (bottom-aligned, minimized toolbar, search tab role), see axiom-swiftui-26-ref.
Part 1: The searchable Modifier
Core API
.searchable(
text: Binding<String>,
placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey
)
Availability: iOS 15+, macOS 12+, tvOS 15+, watchOS 8+
How It Works
- You attach
.searchable(text: $query) to a view
- The nearest navigation container (NavigationStack, NavigationSplitView) renders the search field
- The view receives
isSearching and dismissSearch through the environment
- Your view filters or queries based on the bound text
struct RecipeListView: View {
@State private var searchText = ""
let recipes: [Recipe]
var body: some View {
NavigationStack {
List(filteredRecipes) { recipe in
NavigationLink(recipe.name, value: recipe)
}
.navigationTitle("Recipes")
.searchable(text: $searchText, prompt: "Find a recipe")
}
}
var filteredRecipes: [Recipe] {
if searchText.isEmpty { return recipes }
return recipes.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
}
Placement Options
| Placement |
Behavior |
.automatic |
System decides (recommended) |
.navigationBarDrawer |
Below navigation bar title (iOS) |
.navigationBarDrawer(displayMode: .always) |
Always visible, not hidden on scroll |
.sidebar |
In the sidebar column (NavigationSplitView) |
.toolbar |
In the toolbar area |
.toolbarPrincipal |
In toolbar's principal section |
Gotcha: SwiftUI may ignore your placement preference if the view hierarchy doesn't support it. Always test on the target platform.
Column Association in NavigationSplitView
Where you attach .searchable determines which column displays the search field:
NavigationSplitView {
SidebarView()
.searchable(text: $query)
} detail: {
DetailView()
}
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
.searchable(text: $query)
}
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
}
.searchable(text: $query)
Part 2: Displaying Search Results
isSearching Environment
@Environment(\.isSearching) private var isSearching
Availability: iOS 15+
Becomes true when the user activates search (taps the field), false when they cancel or you call dismissSearch.
Critical rule: isSearching must be read from a child of the view that has .searchable. SwiftUI sets the value in the searchable view's environment and does not propagate it upward.
struct WeatherCityList: View {
@State private var searchText = ""
var body: some View {
NavigationStack {
SearchResultsOverlay(searchText: searchText) {
List(favoriteCities) { city in
CityRow(city: city)
}
}
.searchable(text: $searchText)
.navigationTitle("Weather")
}
}
}
struct SearchResultsOverlay<Content: View>: View {
let searchText: String
@ViewBuilder let content: Content
@Environment(\.isSearching) private var isSearching
var body: some View {
if isSearching {
SearchResults(query: searchText)
} else {
content
}
}
}
dismissSearch Environment
@Environment(\.dismissSearch) private var dismissSearch
Availability: iOS 15+
Calling dismissSearch() clears the search text, removes focus, and sets isSearching to false. Must be called from inside the searchable view hierarchy.
struct SearchResults: View {
@Environment(\.dismissSearch) private var dismissSearch
var body: some View {
List(results) { result in
Button(result.name) {
selectResult(result)
dismissSearch()
}
}
}
}
Part 3: Search Suggestions
Adding Suggestions
Pass a suggestions closure to .searchable:
.searchable(text: $searchText) {
ForEach(suggestedResults) { suggestion in
Text(suggestion.name)
.searchCompletion(suggestion.name)
}
}
Availability: iOS 15+
Suggestions appear in a list below the search field when the user is typing.
searchCompletion Modifier
.searchCompletion(_:) binds a suggestion to a completion value. When the user taps the suggestion, the search text is replaced with the completion value.
.searchable(text: $searchText) {
ForEach(matchingColors) { color in
HStack {
<