安装
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-search-ref
- 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
- )
- // Search in sidebar
- }
- detail
- :
- {
- DetailView
- (
- )
- }
- // vs.
- NavigationSplitView
- {
- SidebarView
- (
- )
- }
- detail
- :
- {
- DetailView
- (
- )
- .
- searchable
- (
- text
- :
- $query
- )
- // Search in detail
- }
- // vs.
- NavigationSplitView
- {
- SidebarView
- (
- )
- }
- detail
- :
- {
- DetailView
- (
- )
- }
- .
- searchable
- (
- text
- :
- $query
- )
- // System decides column
- 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.
- // Pattern: Overlay search results when searching
- struct
- WeatherCityList
- :
- View
- {
- @State
- private
- var
- searchText
- =
- ""
- var
- body
- :
- some
- View
- {
- NavigationStack
- {
- // SearchResultsOverlay reads isSearching
- 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
- {
- // Show search results
- 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
- (
- )
- // Close search after selection
- }
- }
- }
- }
- 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
- {
- Circle
- (
- )
- .
- fill
- (
- color
- .
- value
- )
- .
- frame
- (
- width
- :
- 16
- ,
- height
- :
- 16
- )
- Text
- (
- color
- .
- name
- )
- }
- .
- searchCompletion
- (
- color
- .
- name
- )
- // Tapping fills search with color name
- }
- }
- Without
- .searchCompletion()
-
- Suggestions display but tapping them does nothing to the search field. This is the most common suggestions bug.
- Complete Suggestion Pattern
- struct
- ColorSearchView
- :
- View
- {
- @State
- private
- var
- searchText
- =
- ""
- let
- allColors
- :
- [
- NamedColor
- ]
- var
- body
- :
- some
- View
- {
- NavigationStack
- {
- List
- (
- filteredColors
- )
- {
- color
- in
- ColorRow
- (
- color
- :
- color
- )
- }
- .
- navigationTitle
- (
- "Colors"
- )
- .
- searchable
- (
- text
- :
- $searchText
- ,
- prompt
- :
- "Search colors"
- )
- {
- ForEach
- (
- suggestedColors
- )
- {
- color
- in
- Label
- (
- color
- .
- name
- ,
- systemImage
- :
- "paintpalette"
- )
- .
- searchCompletion
- (
- color
- .
- name
- )
- }
- }
- }
- }
- var
- suggestedColors
- :
- [
- NamedColor
- ]
- {
- guard
- !
- searchText
- .
- isEmpty
- else
- {
- return
- [
- ]
- }
- return
- allColors
- .
- filter
- {
- $0
- .
- name
- .
- localizedCaseInsensitiveContains
- (
- searchText
- )
- }
- .
- prefix
- (
- 5
- )
- .
- map
- {
- $0
- }
- // Convert ArraySlice to Array
- }
- var
- filteredColors
- :
- [
- NamedColor
- ]
- {
- if
- searchText
- .
- isEmpty
- {
- return
- allColors
- }
- return
- allColors
- .
- filter
- {
- $0
- .
- name
- .
- localizedCaseInsensitiveContains
- (
- searchText
- )
- }
- }
- }
- Part 4: Search Submission
- onSubmit(of: .search)
- Triggers when the user presses Return/Enter in the search field:
- .
- searchable
- (
- text
- :
- $searchText
- )
- .
- onSubmit
- (
- of
- :
- .
- search
- )
- {
- performSearch
- (
- searchText
- )
- }
- Availability
-
- iOS 15+
- Filter vs Submit Decision
- Pattern
- Use When
- Example
- Filter-as-you-type
- Local data, fast filtering
- Contacts, settings
- Submit-based search
- Network requests, expensive queries
- App Store, web search
- Combined
- Suggestions filter locally, submit triggers server
- Maps, shopping
- Combined Suggestions + Submit Pattern
- struct
- StoreSearchView
- :
- View
- {
- @State
- private
- var
- searchText
- =
- ""
- @State
- private
- var
- searchResults
- :
- [
- Product
- ]
- =
- [
- ]
- let
- recentSearches
- :
- [
- String
- ]
- var
- body
- :
- some
- View
- {
- NavigationStack
- {
- List
- (
- searchResults
- )
- {
- product
- in
- ProductRow
- (
- product
- :
- product
- )
- }
- .
- navigationTitle
- (
- "Store"
- )
- .
- searchable
- (
- text
- :
- $searchText
- ,
- prompt
- :
- "Search products"
- )
- {
- // Local suggestions from recent searches
- ForEach
- (
- matchingRecent
- ,
- id
- :
- \
- .
- self
- )
- {
- term
- in
- Label
- (
- term
- ,
- systemImage
- :
- "clock"
- )
- .
- searchCompletion
- (
- term
- )
- }
- }
- .
- onSubmit
- (
- of
- :
- .
- search
- )
- {
- // Server search on submit
- Task
- {
- searchResults
- =
- await
- ProductAPI
- .
- search
- (
- searchText
- )
- }
- }
- }
- }
- var
- matchingRecent
- :
- [
- String
- ]
- {
- guard
- !
- searchText
- .
- isEmpty
- else
- {
- return
- recentSearches
- }
- return
- recentSearches
- .
- filter
- {
- $0
- .
- localizedCaseInsensitiveContains
- (
- searchText
- )
- }
- }
- }
- Part 5: Search Scopes (iOS 16+)
- Adding Scopes
- Scopes add a segmented picker below the search field for narrowing results by category:
- enum
- SearchScope
- :
- String
- ,
- CaseIterable
- {
- case
- all
- =
- "All"
- case
- recipes
- =
- "Recipes"
- case
- ingredients
- =
- "Ingredients"
- }
- struct
- ScopedSearchView
- :
- View
- {
- @State
- private
- var
- searchText
- =
- ""
- @State
- private
- var
- searchScope
- :
- SearchScope
- =
- .
- all
- var
- body
- :
- some
- View
- {
- NavigationStack
- {
- List
- (
- filteredResults
- )
- {
- result
- in
- ResultRow
- (
- result
- :
- result
- )
- }
- .
- navigationTitle
- (
- "Cookbook"
- )
- .
- searchable
- (
- text
- :
- $searchText
- )
- .
- searchScopes
- (
- $searchScope
- )
- {
- ForEach
- (
- SearchScope
- .
- allCases
- ,
- id
- :
- \
- .
- self
- )
- {
- scope
- in
- Text
- (
- scope
- .
- rawValue
- )
- .
- tag
- (
- scope
- )
- }
- }
- }
- }
- }
- Availability
-
- iOS 16+, macOS 13+
- Scope Activation (iOS 16.4+)
- Control when scopes appear:
- .
- searchScopes
- (
- $searchScope
- ,
- activation
- :
- .
- onTextEntry
- )
- {
- // Scopes appear only when user starts typing
- ForEach
- (
- SearchScope
- .
- allCases
- ,
- id
- :
- \
- .
- self
- )
- {
- scope
- in
- Text
- (
- scope
- .
- rawValue
- )
- .
- tag
- (
- scope
- )
- }
- }
- Activation
- Behavior
- .automatic
- System default
- .onTextEntry
- Scopes appear when user types text
- .onSearchPresentation
- Scopes appear when search is activated
- Platform differences
- :
- iOS/iPadOS
-
- Scopes appear on text entry by default, dismiss on cancel
- macOS
-
- Scopes appear when search is presented, dismiss on cancel
- Part 6: Search Tokens (iOS 16+)
- Tokens are structured search elements that appear as "pills" in the search field alongside free text.
- Basic Tokens
- enum
- RecipeToken
- :
- Identifiable
- ,
- Hashable
- {
- case
- cuisine
- (
- String
- )
- case
- difficulty
- (
- String
- )
- var
- id
- :
- Self
- {
- self
- }
- }
- struct
- TokenSearchView
- :
- View
- {
- @State
- private
- var
- searchText
- =
- ""
- @State
- private
- var
- tokens
- :
- [
- RecipeToken
- ]
- =
- [
- ]
- var
- body
- :
- some
- View
- {
- NavigationStack
- {
- List
- (
- filteredRecipes
- )
- {
- recipe
- in
- RecipeRow
- (
- recipe
- :
- recipe
- )
- }
- .
- navigationTitle
- (
- "Recipes"
- )
- .
- searchable
- (
- text
- :
- $searchText
- ,
- tokens
- :
- $tokens
- )
- {
- token
- in
- switch
- token
- {
- case
- .
- cuisine
- (
- let
- name
- )
- :
- Label
- (
- name
- ,
- systemImage
- :
- "globe"
- )
- case
- .
- difficulty
- (
- let
- name
- )
- :
- Label
- (
- name
- ,
- systemImage
- :
- "star"
- )
- }
- }
- }
- }
- }
- Availability
-
- iOS 16+
- Token model requirements
-
- Each token element must conform to
- Identifiable
- .
- Suggested Tokens (iOS 17+)
- .
- searchable
- (
- text
- :
- $searchText
- ,
- tokens
- :
- $tokens
- ,
- suggestedTokens
- :
- $suggestedTokens
- ,
- prompt
- :
- "Search recipes"
- )
- {
- token
- in
- Label
- (
- token
- .
- displayName
- ,
- systemImage
- :
- token
- .
- icon
- )
- }
- Availability
-
- iOS 17+ adds
- suggestedTokens
- and
- isPresented
- parameters.
- Combined Tokens + Text Filtering
- var
- filteredRecipes
- :
- [
- Recipe
- ]
- {
- var
- results
- =
- allRecipes
- // Apply token filters
- for
- token
- in
- tokens
- {
- switch
- token
- {
- case
- .
- cuisine
- (
- let
- cuisine
- )
- :
- results
- =
- results
- .
- filter
- {
- $0
- .
- cuisine
- ==
- cuisine
- }
- case
- .
- difficulty
- (
- let
- difficulty
- )
- :
- results
- =
- results
- .
- filter
- {
- $0
- .
- difficulty
- ==
- difficulty
- }
- }
- }
- // Apply text filter
- if
- !
- searchText
- .
- isEmpty
- {
- results
- =
- results
- .
- filter
- {
- $0
- .
- name
- .
- localizedCaseInsensitiveContains
- (
- searchText
- )
- }
- }
- return
- results
- }
- Part 7: Programmatic Search Control (iOS 18+)
- searchFocused
- Bind a
- FocusState
- to the search field to activate or dismiss search programmatically:
- struct
- ProgrammaticSearchView
- :
- View
- {
- @State
- private
- var
- searchText
- =
- ""
- @FocusState
- private
- var
- isSearchFocused
- :
- Bool
- var
- body
- :
- some
- View
- {
- NavigationStack
- {
- VStack
- {
- Button
- (
- "Start Search"
- )
- {
- isSearchFocused
- =
- true
- // Activate search field
- }
- List
- (
- filteredItems
- )
- {
- item
- in
- Text
- (
- item
- .
- name
- )
- }
- }
- .
- navigationTitle
- (
- "Items"
- )
- .
- searchable
- (
- text
- :
- $searchText
- )
- .
- searchFocused
- (
- $isSearchFocused
- )
- }
- }
- }
- Availability
-
- iOS 18+, macOS 15+, visionOS 2+
- Note
-
- For a non-boolean variant, use
- .searchFocused(_:equals:)
- to match specific focus values.
- Comparison with dismissSearch
- API
- Direction
- iOS
- dismissSearch
- Dismiss only
- 15+
- .searchFocused($bool)
- Activate or dismiss
- 18+
- Use
- dismissSearch
- if you only need to close search. Use
- searchFocused
- when you need to programmatically
- open
- search (e.g., a floating action button that opens search).
- Part 8: Platform Behavior
- SwiftUI search adapts automatically per platform:
- Platform
- Default Behavior
- iOS
- Search bar in navigation bar. Scrolls out of view by default; pull down to reveal.
- iPadOS
- Same as iOS in compact; may appear in toolbar in regular width.
- macOS
- Trailing toolbar search field. Always visible.
- watchOS
- Dictation-first input. Search bar at top of list.
- tvOS
- Tab-based search with on-screen keyboard.
- iOS-Specific Behavior
- // Always-visible search field (doesn't scroll away)
- .
- searchable
- (
- text
- :
- $searchText
- ,
- placement
- :
- .
- navigationBarDrawer
- (
- displayMode
- :
- .
- always
- )
- )
- // Default: search field scrolls out, pull down to reveal
- .
- searchable
- (
- text
- :
- $searchText
- )
- macOS-Specific Behavior
- // Search in toolbar (default on macOS)
- .
- searchable
- (
- text
- :
- $searchText
- ,
- placement
- :
- .
- toolbar
- )
- // Search in sidebar
- .
- searchable
- (
- text
- :
- $searchText
- ,
- placement
- :
- .
- sidebar
- )
- Part 9: Common Gotchas
- 1. Search Field Doesn't Appear
- Cause
- :
- .searchable
- is not inside a navigation container.
- // WRONG: No navigation container
- List
- {
- ...
- }
- .
- searchable
- (
- text
- :
- $query
- )
- // CORRECT: Inside NavigationStack
- NavigationStack
- {
- List
- {
- ...
- }
- .
- searchable
- (
- text
- :
- $query
- )
- }
- 2. isSearching Always Returns false
- Cause
-
- Reading
- isSearching
- from the wrong view level.
- // WRONG: Reading from parent of searchable view
- struct
- ParentView
- :
- View
- {
- @Environment
- (
- \
- .
- isSearching
- )
- var
- isSearching
- // Always false
- @State
- private
- var
- query
- =
- ""
- var
- body
- :
- some
- View
- {
- NavigationStack
- {
- ChildView
- (
- isSearching
- :
- isSearching
- )
- .
- searchable
- (
- text
- :
- $query
- )
- }
- }
- }
- // CORRECT: Reading from child view
- struct
- ChildView
- :
- View
- {
- @Environment
- (
- \
- .
- isSearching
- )
- var
- isSearching
- // Works
- var
- body
- :
- some
- View
- {
- if
- isSearching
- {
- SearchResults
- (
- )
- }
- else
- {
- DefaultContent
- (
- )
- }
- }
- }
- 3. Suggestions Don't Fill Search Field
- Cause
-
- Missing
- .searchCompletion()
- on suggestion views.
- // WRONG: No searchCompletion
- .
- searchable
- (
- text
- :
- $query
- )
- {
- ForEach
- (
- suggestions
- )
- {
- s
- in
- Text
- (
- s
- .
- name
- )
- // Displays but tapping does nothing
- }
- }
- // CORRECT: With searchCompletion
- .
- searchable
- (
- text
- :
- $query
- )
- {
- ForEach
- (
- suggestions
- )
- {
- s
- in
- Text
- (
- s
- .
- name
- )
- .
- searchCompletion
- (
- s
- .
- name
- )
- // Fills search field on tap
- }
- }
- 4. Placement on Wrong Navigation Level
- Cause
-
- Attaching
- .searchable
- to the wrong column in NavigationSplitView.
- // Might not appear where expected
- NavigationSplitView
- {
- SidebarView
- (
- )
- }
- detail
- :
- {
- DetailView
- (
- )
- }
- .
- searchable
- (
- text
- :
- $query
- )
- // System chooses column
- // Explicit placement
- NavigationSplitView
- {
- SidebarView
- (
- )
- .
- searchable
- (
- text
- :
- $query
- ,
- placement
- :
- .
- sidebar
- )
- // In sidebar
- }
- detail
- :
- {
- DetailView
- (
- )
- }
- 5. Search Scopes Don't Appear
- Cause
-
- Scopes require
- .searchable
- on the same view. They also require a navigation container.
- // WRONG: Scopes without searchable
- List
- {
- ...
- }
- .
- searchScopes
- (
- $scope
- )
- {
- ...
- }
- // CORRECT: Scopes alongside searchable
- List
- {
- ...
- }
- .
- searchable
- (
- text
- :
- $query
- )
- .
- searchScopes
- (
- $scope
- )
- {
- Text
- (
- "All"
- )
- .
- tag
- (
- Scope
- .
- all
- )
- Text
- (
- "Recent"
- )
- .
- tag
- (
- Scope
- .
- recent
- )
- }
- 6. iOS 26 Refinements
- For bottom-aligned search,
- .searchToolbarBehavior(.minimize)
- ,
- Tab(role: .search)
- , and
- DefaultToolbarItem(kind: .search)
- , see
- axiom-swiftui-26-ref
- . These build on the foundational APIs documented here.
- Part 10: API Quick Reference
- Modifiers
- Modifier
- iOS
- Purpose
- .searchable(text:placement:prompt:)
- 15+
- Add search field
- .searchable(text:tokens:token:)
- 16+
- Search with tokens
- .searchable(text:tokens:suggestedTokens:isPresented:token:)
- 17+
- Tokens + suggested tokens + presentation control
- .searchCompletion(_:)
- 15+
- Auto-fill search on suggestion tap
- .searchScopes(::)
- 16+
- Category picker below search
- .searchScopes(:activation::)
- 16.4+
- Scopes with activation control
- .searchFocused(_:)
- 18+
- Programmatic search focus
- .searchPresentationToolbarBehavior(_:)
- 17.1+
- Keep title visible during search
- .searchToolbarBehavior(_:)
- 26+
- Compact/minimize search field
- onSubmit(of: .search)
- 15+
- Handle search submission
- Environment Values
- Value
- iOS
- Purpose
- isSearching
- 15+
- Is user actively searching
- dismissSearch
- 15+
- Action to dismiss search
- Types
- Type
- iOS
- Purpose
- SearchFieldPlacement
- 15+
- Where search field renders
- SearchScopeActivation
- 16.4+
- When scopes appear
- Resources
- WWDC
-
- 2021-10176, 2022-10023
- Docs
- /swiftui/view/searchable(text:placement:prompt:), /swiftui/environmentvalues/issearching, /swiftui/view/searchscopes(
- :activation:
- :), /swiftui/view/searchfocused(_:), /swiftui/searchfieldplacement
- Skills
- axiom-swiftui-26-ref, axiom-swiftui-nav-ref, axiom-swiftui-nav
Last Updated
Based on WWDC 2021-10176 "Searchable modifier", sosumi.ai API reference
Platforms
iOS 15+, iPadOS 15+, macOS 12+, watchOS 8+, tvOS 15+
← 返回排行榜