axiom-swiftui-search-ref

安装量: 101
排名: #8225

安装

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+
返回排行榜