关于 axiom-swiftui-search-ref
面向 SwiftUI 开发者的搜索 API 参考技能,涵盖不同系统版本的搜索行为、配置与常见问题,重点说明搜索字段与导航容器的关系。
主要功能
- 01searchable API 与位置配置
- 02搜索建议与自动补全
- 03搜索范围与结构化令牌
- 04搜索焦点控制与提交
- 05平台行为与问题排查
适用场景
- 01为 SwiftUI 列表添加搜索
- 02筛选结果或提交查询
- 03构建范围或令牌搜索
- 04排查搜索字段未显示
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swiftui-search-ref安装后在 Claude Code 中输入/axiom-swiftui-search-ref确认已加载。
详细介绍
SwiftUI 搜索 API 参考
概述
SwiftUI 搜索是基于环境并由导航容器使用的。你可以将 .searchable() 附加到视图,但实际的搜索字段由导航容器(NavigationStack、NavigationSplitView 或 TabView)呈现。这种间接机制是大多数搜索问题的根源。
API 演进
| iOS | 主要新增功能 |
|---|---|
| 15 | .searchable(text:)、isSearching、dismissSearch、建议、.searchCompletion()、onSubmit(of: .search) |
| 16 | 搜索范围(.searchScopes)、搜索令牌(.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 |
何时使用此技能
- 为 SwiftUI 列表或集合添加搜索
- 实现输入即筛选或提交后搜索
- 添加带自动补全的搜索建议
- 使用搜索范围按类别缩小结果
- 使用搜索令牌构建结构化查询
- 以编程方式控制搜索焦点
- 调试“搜索字段未显示”问题
有关 iOS 26 的搜索功能(底部对齐、最小化工具栏、搜索标签页角色),请参见 axiom-swiftui-26-ref。
第 1 部分:searchable 修饰符
核心 API
.searchable(
text: Binding<String>,
placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey
)
可用性:iOS 15+、macOS 12+、tvOS 15+、watchOS 8+
工作原理
- 将
.searchable(text: $query)附加到视图 - 最近的导航容器(NavigationStack、NavigationSplitView)会呈现搜索字段
- 视图通过环境接收
isSearching和dismissSearch - 视图会根据绑定的文本筛选或查询
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) }
}
}
位置选项
| 位置 | 行为 |
|---|---|
.automatic | 由系统决定(推荐) |
.navigationBarDrawer | 位于导航栏标题下方(iOS) |
.navigationBarDrawer(displayMode: .always) | 始终显示,滚动时不会隐藏 |
.sidebar | 位于侧边栏列中(NavigationSplitView) |
.toolbar | 位于工具栏区域 |
.toolbarPrincipal | 位于工具栏的主体区域 |
注意:如果视图层级不支持你指定的位置,SwiftUI 可能会忽略你的偏好。请务必在目标平台上进行测试。
NavigationSplitView 中的列关联
.searchable 的附加位置决定了搜索字段显示在哪一列:
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
第 2 部分:显示搜索结果
isSearching 环境值
@Environment(\.isSearching) private var isSearching
可用性:iOS 15+
用户激活搜索(轻点搜索字段)时变为 true;用户取消搜索或调用 dismissSearch 时变为 false。
关键规则:必须从带有 .searchable 的视图的子视图读取 isSearching。SwiftUI 会在可搜索视图的环境中设置此值,但不会向上传递。
// 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(\.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() // Close search after selection
}
}
}
}
第 3 部分:搜索建议
添加建议
向 .searchable 传入 suggestions 闭包:
.searchable(text: $searchText) {
ForEach(suggestedResults) { suggestion in
Text(suggestion.name)
.searchCompletion(suggestion.name)
}
}
可用性:iOS 15+
用户输入时,建议会显示在搜索字段下方的列表中。
searchCompletion 修饰符
.searchCompletion(_:) 会将一条建议绑定到补全值。用户轻点建议时,搜索文本会替换为该补全值。
.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
}
}
不使用 .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 } // Convert ArraySlice to Array
}
var filteredColors: [NamedColor] {
if searchText.isEmpty { return allColors }
return allColors.filter {
$0.name.localizedCaseInsensitiveContains(searchText)
}
}
}
第 4 部分:提交搜索
onSubmit(of: .search)
用户在搜索字段中按下 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") {
// 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)
}
}
}
第 5 部分:搜索范围(iOS 16+)
添加范围
搜索范围会在搜索字段下方添加分段选择器,用于按类别缩小结果范围:
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+
范围激活(iOS 16.4+)
控制范围的显示时机:
.searchScopes($searchScope, activation: .onTextEntry) {
// Scopes appear only when user starts typing
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.rawValue).tag(scope)
}
}
| 激活方式 | 行为 |
|---|---|
.automatic | 系统默认 |
.onTextEntry | 用户输入文本时显示范围 |
.onSearchPresentation | 激活搜索时显示范围 |
平台差异:
- iOS/iPadOS:默认在输入文本时显示范围,取消时隐藏
- macOS:显示搜索界面时显示范围,取消时隐藏
第 6 部分:搜索令牌(iOS 16+)
令牌是结构化的搜索元素,会以“胶囊”形式与自由文本一同显示在搜索字段中。
基本令牌
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。
建议令牌(iOS 17+)
.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
// 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
}
第 7 部分:以编程方式控制搜索(iOS 18+)
searchFocused
将 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 // Activate search field
}
List(filteredItems) { item in
Text(item.name)
}
}
.navigationTitle("Items")
.searchable(text: $searchText)
.searchFocused($isSearchFocused)
}
}
}
可用性:iOS 18+、macOS 15+、visionOS 2+
注意:对于非布尔类型的变体,请使用 .searchFocused(_:equals:) 来匹配特定的焦点值。
与 dismissSearch 的比较
| API | 作用 | iOS |
|---|---|---|
dismissSearch | 仅隐藏 | 15+ |
.searchFocused($bool) | 激活或隐藏 | 18+ |
如果只需关闭搜索,请使用 dismissSearch。如果需要以编程方式打开搜索(例如,通过浮动操作按钮打开搜索),请使用 searchFocused。
第 8 部分:平台行为
SwiftUI 搜索会根据平台自动适配:
| 平台 | 默认行为 |
|---|---|
| iOS | 导航栏中的搜索栏。默认滚动出视图;下拉即可显示。 |
| iPadOS | 紧凑宽度下与 iOS 相同;常规宽度下可能显示在工具栏中。 |
| macOS | 工具栏尾部的搜索字段。始终可见。 |
| watchOS | 以听写为优先的输入方式。搜索栏位于列表顶部。 |
| tvOS | 基于标签页的搜索,使用屏幕键盘。 |
iOS 特定行为
// 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 特定行为
// Search in toolbar (default on macOS)
.searchable(text: $searchText, placement: .toolbar)
// Search in sidebar
.searchable(text: $searchText, placement: .sidebar)
第 9 部分:常见问题
1. 搜索字段未显示
原因:.searchable 不在导航容器内。
// WRONG: No navigation container
List { ... }
.searchable(text: $query)
// CORRECT: Inside NavigationStack
NavigationStack {
List { ... }
.searchable(text: $query)
}
2. isSearching 始终返回 false
原因:从错误的视图层级读取 isSearching。
// 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. 建议未填入搜索字段
原因:建议视图缺少 .searchCompletion()。
// 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. 放置在错误的导航层级
原因:在 NavigationSplitView 中将 .searchable 附加到了错误的列。
// 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. 搜索范围未显示
原因:范围要求 .searchable 位于同一视图上,也要求存在导航容器。
// 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 改进
有关底部对齐搜索、.searchToolbarBehavior(.minimize)、Tab(role: .search) 和 DefaultToolbarItem(kind: .search),请参阅 axiom-swiftui-26-ref。这些内容以本文档介绍的基础 API 为基础。
第 10 部分: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+