Loading...
Loading...
Compare original and translation side by side
.searchable().searchable()| iOS | Key Additions |
|---|---|
| 15 | |
| 16 | Search scopes ( |
| 16.4 | Search scope |
| 17 | |
| 17.1 | |
| 18 | |
| 26 | Bottom-aligned search, |
| iOS版本 | 关键新增功能 |
|---|---|
| 15 | |
| 16 | 搜索范围( |
| 16.4 | 搜索范围 |
| 17 | |
| 17.1 | |
| 18 | |
| 26 | 底部对齐搜索、 |
axiom-swiftui-26-refaxiom-swiftui-26-ref.searchable(
text: Binding<String>,
placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey
).searchable(
text: Binding<String>,
placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey
).searchable(text: $query)isSearchingdismissSearchstruct 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) }
}
}.searchable(text: $query)isSearchingdismissSearchstruct 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 |
|---|---|
| System decides (recommended) |
| Below navigation bar title (iOS) |
| Always visible, not hidden on scroll |
| In the sidebar column (NavigationSplitView) |
| In the toolbar area |
| In toolbar's principal section |
| 位置 | 行为 |
|---|---|
| 由系统决定(推荐) |
| 位于导航栏标题下方(iOS) |
| 始终可见,滚动时不会隐藏 |
| 位于侧边栏列中(NavigationSplitView) |
| 位于工具栏区域 |
| 位于工具栏的主区域 |
.searchableNavigationSplitView {
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.searchableNavigationSplitView {
SidebarView()
.searchable(text: $query) // 搜索栏在侧边栏
} detail: {
DetailView()
}
// 对比
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
.searchable(text: $query) // 搜索栏在详情页
}
// 对比
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
}
.searchable(text: $query) // 由系统决定显示列@Environment(\.isSearching) private var isSearchingtruefalsedismissSearchisSearching.searchable// 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(\.isSearching) private var isSearchingtruedismissSearchfalseisSearching// 模式:搜索时覆盖显示搜索结果
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 dismissSearchdismissSearch()isSearchingfalsestruct 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
}
}
}
}@Environment(\.dismissSearch) private var dismissSearchdismissSearch()isSearchingfalsestruct SearchResults: View {
@Environment(\.dismissSearch) private var dismissSearch
var body: some View {
List(results) { result in
Button(result.name) {
selectResult(result)
dismissSearch() // 选择结果后关闭搜索
}
}
}
}suggestions.searchable.searchable(text: $searchText) {
ForEach(suggestedResults) { suggestion in
Text(suggestion.name)
.searchCompletion(suggestion.name)
}
}.searchablesuggestions.searchable(text: $searchText) {
ForEach(suggestedResults) { suggestion in
Text(suggestion.name)
.searchCompletion(suggestion.name)
}
}.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().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 } // Convert ArraySlice to Array
}
var filteredColors: [NamedColor] {
if searchText.isEmpty { return allColors }
return allColors.filter {
$0.name.localizedCaseInsensitiveContains(searchText)
}
}
}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)
}
}
}.searchable(text: $searchText)
.onSubmit(of: .search) {
performSearch(searchText)
}.searchable(text: $searchText)
.onSubmit(of: .search) {
performSearch(searchText)
}| 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 |
| 模式 | 使用场景 | 示例 |
|---|---|---|
| 输入即过滤 | 本地数据、快速过滤 | 联系人、设置 |
| 提交式搜索 | 网络请求、耗时查询 | 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)
}
}
}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)
}
}
}
}
}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)
}
}
}
}
}.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 |
|---|---|
| System default |
| Scopes appear when user types text |
| Scopes appear when search is activated |
.searchScopes($searchScope, activation: .onTextEntry) {
// 仅当用户开始输入时显示范围
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.rawValue).tag(scope)
}
}| 激活时机 | 行为 |
|---|---|
| 系统默认 |
| 用户输入文本时显示范围 |
| 激活搜索时显示范围 |
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")
}
}
}
}
}Identifiableenum 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")
}
}
}
}
}Identifiable.searchable(
text: $searchText,
tokens: $tokens,
suggestedTokens: $suggestedTokens,
prompt: "Search recipes"
) { token in
Label(token.displayName, systemImage: token.icon)
}suggestedTokensisPresented.searchable(
text: $searchText,
tokens: $tokens,
suggestedTokens: $suggestedTokens,
prompt: "Search recipes"
) { token in
Label(token.displayName, systemImage: token.icon)
}suggestedTokensisPresentedvar 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
}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 // Activate search field
}
List(filteredItems) { item in
Text(item.name)
}
}
.navigationTitle("Items")
.searchable(text: $searchText)
.searchFocused($isSearchFocused)
}
}
}.searchFocused(_:equals:)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)
}
}
}.searchFocused(_:equals:)| API | Direction | iOS |
|---|---|---|
| Dismiss only | 15+ |
| Activate or dismiss | 18+ |
dismissSearchsearchFocused| API | 功能方向 | iOS版本 |
|---|---|---|
| 仅支持关闭 | 15+ |
| 支持激活或关闭 | 18+ |
dismissSearchsearchFocused| 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 | 搜索栏位于导航栏中。默认会随滚动隐藏;下拉可重新显示。 |
| iPadOS | 紧凑布局下与iOS相同;常规宽度下可能显示在工具栏中。 |
| macOS | 工具栏右侧的搜索栏。始终可见。 |
| watchOS | 以语音输入优先。搜索栏位于列表顶部。 |
| tvOS | 基于标签页的搜索,搭配屏幕键盘。 |
// 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)// 始终可见的搜索栏(不会随滚动隐藏)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
// 默认:搜索栏随滚动隐藏,下拉可显示
.searchable(text: $searchText)// Search in toolbar (default on macOS)
.searchable(text: $searchText, placement: .toolbar)
// Search in sidebar
.searchable(text: $searchText, placement: .sidebar)// 工具栏中的搜索栏(macOS默认)
.searchable(text: $searchText, placement: .toolbar)
// 侧边栏中的搜索栏
.searchable(text: $searchText, placement: .sidebar).searchable// WRONG: No navigation container
List { ... }
.searchable(text: $query)
// CORRECT: Inside NavigationStack
NavigationStack {
List { ... }
.searchable(text: $query)
}.searchable// 错误:没有导航容器
List { ... }
.searchable(text: $query)
// 正确:在NavigationStack内
NavigationStack {
List { ... }
.searchable(text: $query)
}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()
}
}
}isSearching// 错误:从附加了.searchable的视图的父视图读取
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()// 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
}
}.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) // 点击后填入搜索栏
}
}.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()
}// 可能不会显示在预期位置
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
}
.searchable(text: $query) // 由系统选择列
// 明确指定位置
NavigationSplitView {
SidebarView()
.searchable(text: $query, placement: .sidebar) // 在侧边栏
} detail: {
DetailView()
}.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)
}.searchable// 错误:没有searchable的情况下添加scopes
List { ... }
.searchScopes($scope) { ... }
// 正确:scopes与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.searchToolbarBehavior(.minimize)Tab(role: .search)DefaultToolbarItem(kind: .search)axiom-swiftui-26-ref| Modifier | iOS | Purpose |
|---|---|---|
| 15+ | Add search field |
| 16+ | Search with tokens |
| 17+ | Tokens + suggested tokens + presentation control |
| 15+ | Auto-fill search on suggestion tap |
| 16+ | Category picker below search |
| 16.4+ | Scopes with activation control |
| 18+ | Programmatic search focus |
| 17.1+ | Keep title visible during search |
| 26+ | Compact/minimize search field |
| 15+ | Handle search submission |
| 修饰符 | iOS版本 | 用途 |
|---|---|---|
| 15+ | 添加搜索栏 |
| 16+ | 带令牌的搜索 |
| 17+ | 令牌+建议令牌+显示控制 |
| 15+ | 点击建议时自动填充搜索栏 |
| 16+ | 搜索栏下方的类别选择器 |
| 16.4+ | 可控制激活时机的搜索范围 |
| 18+ | 程序化搜索焦点控制 |
| 17.1+ | 搜索时保持标题可见 |
| 26+ | 紧凑/最小化搜索栏 |
| 15+ | 处理搜索提交 |
| Value | iOS | Purpose |
|---|---|---|
| 15+ | Is user actively searching |
| 15+ | Action to dismiss search |
| 值 | iOS版本 | 用途 |
|---|---|---|
| 15+ | 用户是否正在搜索 |
| 15+ | 关闭搜索的操作 |
| Type | iOS | Purpose |
|---|---|---|
| 15+ | Where search field renders |
| 16.4+ | When scopes appear |
| 类型 | iOS版本 | 用途 |
|---|---|---|
| 15+ | 搜索栏的渲染位置 |
| 16.4+ | 搜索范围的激活时机 |