axiom-swiftui-search-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-search-refSwiftUI 搜索是基于环境并由导航容器消费的。你将 .searchable() 附加到一个视图上,但实际的搜索字段是由一个_导航容器_(NavigationStack、NavigationSplitView 或 TabView)渲染的。这种间接性是大多数搜索错误的根源。
| iOS | 关键新增功能 |
|---|---|
| 15 | .searchable(text:)、isSearching、dismissSearch、搜索建议、.searchCompletion()、onSubmit(of: .search) |
| 16 | 搜索范围(.searchScopes)、搜索令牌()、 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
.searchable(text:tokens:)SearchScopeActivation| 16.4 | 搜索范围 activation 参数(.onTextEntry、.onSearchPresentation) |
| 17 | isPresented 参数、suggestedTokens 参数 |
| 17.1 | .searchPresentationToolbarBehavior(.avoidHidingContent) |
| 18 | .searchFocused($isFocused) 用于编程控制焦点 |
| 26 | 底部对齐搜索、.searchToolbarBehavior(.minimize)、Tab(role: .search)、DefaultToolbarItem(kind: .search) — 参见 axiom-swiftui-26-ref |
关于 iOS 26 的搜索功能(底部对齐、最小化工具栏、搜索标签角色),请参见 axiom-swiftui-26-ref。
.searchable(
text: Binding<String>,
placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey
)
可用性 : iOS 15+, macOS 12+, tvOS 15+, watchOS 8+
.searchable(text: $query) 附加到一个视图isSearching 和 dismissSearchstruct 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) }
}
}
| 放置选项 | 行为 |
|---|---|
.automatic | 系统决定(推荐) |
.navigationBarDrawer | 导航栏标题下方(iOS) |
.navigationBarDrawer(displayMode: .always) | 始终可见,滚动时不会隐藏 |
.sidebar | 在侧边栏列中(NavigationSplitView) |
.toolbar | 在工具栏区域 |
.toolbarPrincipal | 在工具栏的主要部分 |
注意 : 如果视图层次结构不支持,SwiftUI 可能会忽略你的放置偏好。请务必在目标平台上测试。
你附加 .searchable 的位置决定了哪个列显示搜索字段:
NavigationSplitView {
SidebarView()
.searchable(text: $query) // 在侧边栏中搜索
} detail: {
DetailView()
}
// 对比
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
.searchable(text: $query) // 在详情视图中搜索
}
// 对比
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
}
.searchable(text: $query) // 系统决定列
@Environment(\.isSearching) private var isSearching
可用性 : iOS 15+
当用户激活搜索(点击字段)时变为 true,当他们取消或你调用 dismissSearch 时变为 false。
关键规则 : isSearching 必须从具有 .searchable 的视图的子视图中读取。SwiftUI 在可搜索视图的环境中设置该值,并且不会向上传播。
// 模式:搜索时覆盖显示搜索结果
struct WeatherCityList: View {
@State private var searchText = ""
var body: some View {
NavigationStack {
// SearchResultsOverlay 读取 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 {
// 显示搜索结果
SearchResults(query: searchText)
} else {
content
}
}
}
@Environment(\.dismissSearch) private var dismissSearch
可用性 : iOS 15+
调用 dismissSearch() 会清除搜索文本、移除焦点,并将 isSearching 设置为 false。必须从可搜索视图层次结构内部调用。
struct SearchResults: View {
@Environment(\.dismissSearch) private var dismissSearch
var body: some View {
List(results) { result in
Button(result.name) {
selectResult(result)
dismissSearch() // 选择后关闭搜索
}
}
}
}
向 .searchable 传递一个 suggestions 闭包:
.searchable(text: $searchText) {
ForEach(suggestedResults) { suggestion in
Text(suggestion.name)
.searchCompletion(suggestion.name)
}
}
可用性 : iOS 15+
当用户输入时,建议会出现在搜索字段下方的列表中。
.searchCompletion(_:) 将一个建议绑定到一个补全值。当用户点击建议时,搜索文本会被替换为补全值。
.searchable(text: $searchText) {
ForEach(matchingColors) { color in
HStack {
Circle()
.fill(color.value)
.frame(width: 16, height: 16)
Text(color.name)
}
.searchCompletion(color.name) // 点击将搜索框填充为颜色名称
}
}
没有 .searchCompletion() : 建议会显示,但点击它们不会对搜索字段产生任何影响。这是最常见的建议错误。
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 } // 将 ArraySlice 转换为 Array
}
var filteredColors: [NamedColor] {
if searchText.isEmpty { return allColors }
return allColors.filter {
$0.name.localizedCaseInsensitiveContains(searchText)
}
}
}
当用户在搜索字段中按下 Return/Enter 键时触发:
.searchable(text: $searchText)
.onSubmit(of: .search) {
performSearch(searchText)
}
可用性 : iOS 15+
| 模式 | 适用场景 | 示例 |
|---|---|---|
| 边输入边过滤 | 本地数据,快速过滤 | 联系人,设置 |
| 基于提交的搜索 | 网络请求,昂贵的查询 | App Store,网页搜索 |
| 组合模式 | 建议本地过滤,提交触发服务器 | 地图,购物 |
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") {
// 来自最近搜索的本地建议
ForEach(matchingRecent, id: \.self) { term in
Label(term, systemImage: "clock")
.searchCompletion(term)
}
}
.onSubmit(of: .search) {
// 提交时进行服务器搜索
Task {
searchResults = await ProductAPI.search(searchText)
}
}
}
}
var matchingRecent: [String] {
guard !searchText.isEmpty else { return recentSearches }
return recentSearches.filter {
$0.localizedCaseInsensitiveContains(searchText)
}
}
}
范围在搜索字段下方添加一个分段选择器,用于按类别缩小结果:
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)
}
}
}
}
}
可用性 : iOS 16+, macOS 13+
控制范围何时出现:
.searchScopes($searchScope, activation: .onTextEntry) {
// 范围仅在用户开始输入时出现
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.rawValue).tag(scope)
}
}
| 激活方式 | 行为 |
|---|---|
.automatic | 系统默认 |
.onTextEntry | 用户输入文本时出现范围 |
.onSearchPresentation | 搜索被激活时出现范围 |
平台差异 :
令牌是结构化的搜索元素,在搜索字段中与自由文本一起显示为“药丸”状。
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")
}
}
}
}
}
可用性 : iOS 16+
令牌模型要求 : 每个令牌元素必须符合 Identifiable 协议。
.searchable(
text: $searchText,
tokens: $tokens,
suggestedTokens: $suggestedTokens,
prompt: "Search recipes"
) { token in
Label(token.displayName, systemImage: token.icon)
}
可用性 : iOS 17+ 增加了 suggestedTokens 和 isPresented 参数。
var filteredRecipes: [Recipe] {
var results = allRecipes
// 应用令牌过滤器
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 }
}
}
// 应用文本过滤器
if !searchText.isEmpty {
results = results.filter {
$0.name.localizedCaseInsensitiveContains(searchText)
}
}
return results
}
将 FocusState<Bool> 绑定到搜索字段,以编程方式激活或关闭搜索:
struct ProgrammaticSearchView: View {
@State private var searchText = ""
@FocusState private var isSearchFocused: Bool
var body: some View {
NavigationStack {
VStack {
Button("Start Search") {
isSearchFocused = true // 激活搜索字段
}
List(filteredItems) { item in
Text(item.name)
}
}
.navigationTitle("Items")
.searchable(text: $searchText)
.searchFocused($isSearchFocused)
}
}
}
可用性 : iOS 18+, macOS 15+, visionOS 2+
注意 : 对于非布尔值变体,使用 .searchFocused(_:equals:) 来匹配特定的焦点值。
| API | 方向 | iOS |
|---|---|---|
dismissSearch | 仅关闭 | 15+ |
.searchFocused($bool) | 激活或关闭 | 18+ |
如果你只需要关闭搜索,请使用 dismissSearch。当你需要以编程方式_打开_搜索时(例如,一个打开搜索的浮动操作按钮),请使用 searchFocused。
SwiftUI 搜索会根据平台自动调整:
| 平台 | 默认行为 |
|---|---|
| iOS | 导航栏中的搜索栏。默认滚动出视图;下拉可显示。 |
| iPadOS | 紧凑宽度下与 iOS 相同;常规宽度下可能出现在工具栏中。 |
| macOS | 尾部工具栏搜索字段。始终可见。 |
| watchOS | 听写优先输入。列表顶部的搜索栏。 |
| tvOS | 基于标签的搜索,带屏幕键盘。 |
// 始终可见的搜索字段(不会滚动消失)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
// 默认:搜索字段滚动消失,下拉可显示
.searchable(text: $searchText)
// 工具栏中的搜索(macOS 默认)
.searchable(text: $searchText, placement: .toolbar)
// 侧边栏中的搜索
.searchable(text: $searchText, placement: .sidebar)
原因 : .searchable 不在导航容器内。
// 错误:没有导航容器
List { ... }
.searchable(text: $query)
// 正确:在 NavigationStack 内
NavigationStack {
List { ... }
.searchable(text: $query)
}
原因 : 从错误的视图层级读取 isSearching。
// 错误:从可搜索视图的父视图读取
struct ParentView: View {
@Environment(\.isSearching) var isSearching // 始终为 false
@State private var query = ""
var body: some View {
NavigationStack {
ChildView(isSearching: isSearching)
.searchable(text: $query)
}
}
}
// 正确:从子视图读取
struct ChildView: View {
@Environment(\.isSearching) var isSearching // 有效
var body: some View {
if isSearching {
SearchResults()
} else {
DefaultContent()
}
}
}
原因 : 建议视图上缺少 .searchCompletion()。
// 错误:没有 searchCompletion
.searchable(text: $query) {
ForEach(suggestions) { s in
Text(s.name) // 显示但点击无反应
}
}
// 正确:带有 searchCompletion
.searchable(text: $query) {
ForEach(suggestions) { s in
Text(s.name)
.searchCompletion(s.name) // 点击时填充搜索字段
}
}
原因 : 在 NavigationSplitView 中将 .searchable 附加到了错误的列。
// 可能不会出现在预期位置
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
}
.searchable(text: $query) // 系统选择列
// 显式放置
NavigationSplitView {
SidebarView()
.searchable(text: $query, placement: .sidebar) // 在侧边栏中
} detail: {
DetailView()
}
原因 : 范围要求在同一视图上有 .searchable。它们还需要一个导航容器。
// 错误:有范围但没有 searchable
List { ... }
.searchScopes($scope) { ... }
// 正确:范围与 searchable 一起使用
List { ... }
.searchable(text: $query)
.searchScopes($scope) {
Text("All").tag(Scope.all)
Text("Recent").tag(Scope.recent)
}
关于底部对齐搜索、.searchToolbarBehavior(.minimize)、Tab(role: .search) 和 DefaultToolbarItem(kind: .search),请参见 axiom-swiftui-26-ref。这些功能建立在此处记录的基础 API 之上。
| 修饰符 | iOS | 用途 |
|---|---|---|
.searchable(text:placement:prompt:) | 15+ | 添加搜索字段 |
.searchable(text:tokens:token:) | 16+ | 带令牌的搜索 |
.searchable(text:tokens:suggestedTokens:isPresented:token:) | 17+ | 令牌 + 建议令牌 + 呈现控制 |
.searchCompletion(_:) | 15+ | 点击建议时自动填充搜索 |
.searchScopes(_:_:) | 16+ | 搜索下方的类别选择器 |
.searchScopes(_:activation:_:) | 16.4+ | 带激活控制的范围 |
.searchFocused(_:) | 18+ | 编程式搜索焦点控制 |
.searchPresentationToolbarBehavior(_:) | 17.1+ | 搜索期间保持标题可见 |
.searchToolbarBehavior(_:) | 26+ | 紧凑/最小化搜索字段 |
onSubmit(of: .search) | 15+ | 处理搜索提交 |
| 值 | iOS | 用途 |
|---|---|---|
isSearching | 15+ | 用户是否正在主动搜索 |
dismissSearch | 15+ | 关闭搜索的操作 |
| 类型 | iOS | 用途 |
|---|---|---|
SearchFieldPlacement | 15+ | 搜索字段渲染位置 |
SearchScopeActivation | 16.4+ | 范围出现时机 |
WWDC : 2021-10176, 2022-10023
文档 : /swiftui/view/searchable(text:placement:prompt:), /swiftui/environmentvalues/issearching, /swiftui/view/searchscopes(:activation: :), /swiftui/view/searchfocused(_:), /swiftui/searchfieldplacement
技能 : axiom-swiftui-26-ref, axiom-swiftui-nav-ref, axiom-swiftui-nav
最后更新 基于 WWDC 2021-10176 "Searchable modifier", sosumi.ai API 参考 平台 iOS 15+, iPadOS 15+, macOS 12+, watchOS 8+, tvOS 15+
每周安装量
75
仓库
GitHub 星标
601
首次出现
2026年2月1日
安全审计
已安装于
opencode67
gemini-cli63
codex62
cursor61
github-copilot61
claude-code57
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.
| 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 |
For iOS 26 search features (bottom-aligned, minimized toolbar, search tab role), see axiom-swiftui-26-ref.
.searchable(
text: Binding<String>,
placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey
)
Availability : iOS 15+, macOS 12+, tvOS 15+, watchOS 8+
.searchable(text: $query) to a viewisSearching and dismissSearch through the environmentstruct 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 | 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 |
Gotcha : SwiftUI may ignore your placement preference if the view hierarchy doesn't support it. Always test on the target platform.
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
@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
}
}
}
@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
}
}
}
}
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(_:) 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.
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)
}
}
}
Triggers when the user presses Return/Enter in the search field:
.searchable(text: $searchText)
.onSubmit(of: .search) {
performSearch(searchText)
}
Availability : iOS 15+
| 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 |
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)
}
}
}
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+
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 :
Tokens are structured search elements that appear as "pills" in the search field alongside free text.
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.
.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.
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
}
Bind a FocusState<Bool> 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.
| 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).
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. |
// 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)
// Search in toolbar (default on macOS)
.searchable(text: $searchText, placement: .toolbar)
// Search in sidebar
.searchable(text: $searchText, placement: .sidebar)
Cause : .searchable is not inside a navigation container.
// WRONG: No navigation container
List { ... }
.searchable(text: $query)
// CORRECT: Inside NavigationStack
NavigationStack {
List { ... }
.searchable(text: $query)
}
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()
}
}
}
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
}
}
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()
}
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)
}
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.
| 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(_:_:) |
| Value | iOS | Purpose |
|---|---|---|
isSearching | 15+ | Is user actively searching |
dismissSearch | 15+ | Action to dismiss search |
| Type | iOS | Purpose |
|---|---|---|
SearchFieldPlacement | 15+ | Where search field renders |
SearchScopeActivation | 16.4+ | When scopes appear |
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+
Weekly Installs
75
Repository
GitHub Stars
601
First Seen
Feb 1, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode67
gemini-cli63
codex62
cursor61
github-copilot61
claude-code57
| In toolbar's principal section |
| 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 |