axiom-swiftui-search-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Search API Reference

SwiftUI 搜索API参考

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.
SwiftUI搜索是基于环境且由导航容器托管的。你可以将
.searchable()
附加到任意视图,但实际的搜索栏是由导航容器(NavigationStack、NavigationSplitView或TabView)渲染的。这种间接性是大多数搜索相关bug的根源。

API Evolution

API演进

iOSKey Additions
15
.searchable(text:)
,
isSearching
,
dismissSearch
, suggestions,
.searchCompletion()
,
onSubmit(of: .search)
16Search scopes (
.searchScopes
), search tokens (
.searchable(text:tokens:)
),
SearchScopeActivation
16.4Search scope
activation
parameter (
.onTextEntry
,
.onSearchPresentation
)
17
isPresented
parameter,
suggestedTokens
parameter
17.1
.searchPresentationToolbarBehavior(.avoidHidingContent)
18
.searchFocused($isFocused)
for programmatic focus control
26Bottom-aligned search,
.searchToolbarBehavior(.minimize)
,
Tab(role: .search)
,
DefaultToolbarItem(kind: .search)
— see
axiom-swiftui-26-ref
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

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
.

  • 为SwiftUI列表或集合添加搜索功能
  • 实现输入即过滤或提交式搜索
  • 添加带自动补全的搜索建议
  • 使用搜索范围按类别缩小结果
  • 使用搜索令牌构建结构化查询
  • 程序化控制搜索焦点
  • 排查“搜索栏不显示”等问题
关于iOS 26的搜索功能(底部对齐、最小化工具栏、搜索标签页角色),请查看
axiom-swiftui-26-ref

Part 1: The searchable Modifier

第一部分:.searchable修饰符

Core API

核心API

swift
.searchable(
    text: Binding<String>,
    placement: SearchFieldPlacement = .automatic,
    prompt: LocalizedStringKey
)
Availability: iOS 15+, macOS 12+, tvOS 15+, watchOS 8+
swift
.searchable(
    text: Binding<String>,
    placement: SearchFieldPlacement = .automatic,
    prompt: LocalizedStringKey
)
兼容性:iOS 15+、macOS 12+、tvOS 15+、watchOS 8+

How It Works

工作原理

  1. You attach
    .searchable(text: $query)
    to a view
  2. The nearest navigation container (NavigationStack, NavigationSplitView) renders the search field
  3. The view receives
    isSearching
    and
    dismissSearch
    through the environment
  4. Your view filters or queries based on the bound text
swift
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) }
    }
}
  1. .searchable(text: $query)
    附加到一个视图
  2. 最近的导航容器(NavigationStack、NavigationSplitView)渲染搜索栏
  3. 视图通过环境获取
    isSearching
    dismissSearch
  4. 视图根据绑定的文本进行过滤或查询
swift
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

位置选项

PlacementBehavior
.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.
位置行为
.automatic
由系统决定(推荐)
.navigationBarDrawer
位于导航栏标题下方(iOS)
.navigationBarDrawer(displayMode: .always)
始终可见,滚动时不会隐藏
.sidebar
位于侧边栏列中(NavigationSplitView)
.toolbar
位于工具栏区域
.toolbarPrincipal
位于工具栏的主区域
注意:如果视图层级不支持,SwiftUI可能会忽略你设置的位置偏好。请务必在目标平台上测试。

Column Association in NavigationSplitView

NavigationSplitView中的列关联

Where you attach
.searchable
determines which column displays the search field:
swift
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

.searchable
附加的位置决定了搜索栏显示在哪个列中:
swift
NavigationSplitView {
    SidebarView()
        .searchable(text: $query)  // 搜索栏在侧边栏
} detail: {
    DetailView()
}

// 对比

NavigationSplitView {
    SidebarView()
} detail: {
    DetailView()
        .searchable(text: $query)  // 搜索栏在详情页
}

// 对比

NavigationSplitView {
    SidebarView()
} detail: {
    DetailView()
}
.searchable(text: $query)  // 由系统决定显示列

Part 2: Displaying Search Results

第二部分:显示搜索结果

isSearching Environment

isSearching环境变量

swift
@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.
swift
// 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
        }
    }
}
swift
@Environment(\.isSearching) private var isSearching
兼容性:iOS 15+
当用户激活搜索(点击搜索栏)时变为
true
,当用户取消或调用
dismissSearch
时变为
false
关键规则
isSearching
必须从附加了.searchable的视图的子视图中读取。SwiftUI会在附加了.searchable的视图的环境中设置该值,不会向上传播。
swift
// 模式:搜索时覆盖显示搜索结果
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
        }
    }
}

dismissSearch Environment

dismissSearch环境变量

swift
@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.
swift
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
            }
        }
    }
}

swift
@Environment(\.dismissSearch) private var dismissSearch
兼容性:iOS 15+
调用
dismissSearch()
会清空搜索文本、移除焦点并将
isSearching
设置为
false
。必须在附加了.searchable的视图层级内调用。
swift
struct SearchResults: View {
    @Environment(\.dismissSearch) private var dismissSearch

    var body: some View {
        List(results) { result in
            Button(result.name) {
                selectResult(result)
                dismissSearch()  // 选择结果后关闭搜索
            }
        }
    }
}

Part 3: Search Suggestions

第三部分:搜索建议

Adding Suggestions

添加搜索建议

Pass a
suggestions
closure to
.searchable
:
swift
.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.
.searchable
传递
suggestions
闭包:
swift
.searchable(text: $searchText) {
    ForEach(suggestedResults) { suggestion in
        Text(suggestion.name)
            .searchCompletion(suggestion.name)
    }
}
兼容性:iOS 15+
当用户输入时,建议会显示在搜索栏下方的列表中。

searchCompletion Modifier

searchCompletion修饰符

.searchCompletion(_:)
binds a suggestion to a completion value. When the user taps the suggestion, the search text is replaced with the completion value.
swift
.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.
.searchCompletion(_:)
将建议与补全值绑定。当用户点击建议时,搜索文本会被替换为补全值。
swift
.searchable(text: $searchText) {
    ForEach(matchingColors) { color in
        HStack {
            Circle()
                .fill(color.value)
                .frame(width: 16, height: 16)
            Text(color.name)
        }
        .searchCompletion(color.name)  // 点击后将颜色名称填入搜索栏
    }
}
如果没有
.searchCompletion()
:建议会显示,但点击后不会对搜索栏产生任何影响。这是搜索建议最常见的bug。

Complete Suggestion Pattern

完整的搜索建议模式

swift
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)
        }
    }
}

swift
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)
        }
    }
}

Part 4: Search Submission

第四部分:搜索提交

onSubmit(of: .search)

onSubmit(of: .search)

Triggers when the user presses Return/Enter in the search field:
swift
.searchable(text: $searchText)
.onSubmit(of: .search) {
    performSearch(searchText)
}
Availability: iOS 15+
当用户在搜索栏按下Return/Enter键时触发:
swift
.searchable(text: $searchText)
.onSubmit(of: .search) {
    performSearch(searchText)
}
兼容性:iOS 15+

Filter vs Submit Decision

过滤 vs 提交的决策

PatternUse WhenExample
Filter-as-you-typeLocal data, fast filteringContacts, settings
Submit-based searchNetwork requests, expensive queriesApp Store, web search
CombinedSuggestions filter locally, submit triggers serverMaps, shopping
模式使用场景示例
输入即过滤本地数据、快速过滤联系人、设置
提交式搜索网络请求、耗时查询App Store、网页搜索
组合模式本地建议过滤,提交触发服务器查询地图、购物

Combined Suggestions + Submit Pattern

搜索建议+提交的组合模式

swift
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)
        }
    }
}

swift
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)
        }
    }
}

Part 5: Search Scopes (iOS 16+)

第五部分:搜索范围(iOS 16+)

Adding Scopes

添加搜索范围

Scopes add a segmented picker below the search field for narrowing results by category:
swift
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+
搜索范围会在搜索栏下方添加分段选择器,用于按类别缩小结果范围:
swift
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+

Scope Activation (iOS 16.4+)

范围激活时机(iOS 16.4+)

Control when scopes appear:
swift
.searchScopes($searchScope, activation: .onTextEntry) {
    // Scopes appear only when user starts typing
    ForEach(SearchScope.allCases, id: \.self) { scope in
        Text(scope.rawValue).tag(scope)
    }
}
ActivationBehavior
.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

控制搜索范围的显示时机:
swift
.searchScopes($searchScope, activation: .onTextEntry) {
    // 仅当用户开始输入时显示范围
    ForEach(SearchScope.allCases, id: \.self) { scope in
        Text(scope.rawValue).tag(scope)
    }
}
激活时机行为
.automatic
系统默认
.onTextEntry
用户输入文本时显示范围
.onSearchPresentation
激活搜索时显示范围
平台差异
  • iOS/iPadOS:默认在输入文本时显示范围,取消搜索时隐藏
  • macOS:激活搜索时显示范围,取消搜索时隐藏

Part 6: Search Tokens (iOS 16+)

第六部分:搜索令牌(iOS 16+)

Tokens are structured search elements that appear as "pills" in the search field alongside free text.
搜索令牌是结构化的搜索元素,在搜索栏中以“胶囊”形式与自由文本共存。

Basic Tokens

基础令牌

swift
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
.
swift
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
协议。

Suggested Tokens (iOS 17+)

建议令牌(iOS 17+)

swift
.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.
swift
.searchable(
    text: $searchText,
    tokens: $tokens,
    suggestedTokens: $suggestedTokens,
    prompt: "Search recipes"
) { token in
    Label(token.displayName, systemImage: token.icon)
}
兼容性:iOS 17+新增了
suggestedTokens
isPresented
参数。

Combined Tokens + Text Filtering

令牌+文本过滤的组合

swift
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
}

swift
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
}

Part 7: Programmatic Search Control (iOS 18+)

第七部分:程序化搜索控制(iOS 18+)

searchFocused

searchFocused

Bind a
FocusState<Bool>
to the search field to activate or dismiss search programmatically:
swift
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.
FocusState<Bool>
与搜索栏绑定,以程序化方式激活或关闭搜索:
swift
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:)
匹配特定焦点值。

Comparison with dismissSearch

与dismissSearch的对比

APIDirectioniOS
dismissSearch
Dismiss only15+
.searchFocused($bool)
Activate or dismiss18+
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).

API功能方向iOS版本
dismissSearch
仅支持关闭15+
.searchFocused($bool)
支持激活或关闭18+
如果仅需要关闭搜索,使用
dismissSearch
。当需要程序化打开搜索时(例如,点击浮动操作按钮打开搜索),使用
searchFocused

Part 8: Platform Behavior

第八部分:平台行为

SwiftUI search adapts automatically per platform:
PlatformDefault Behavior
iOSSearch bar in navigation bar. Scrolls out of view by default; pull down to reveal.
iPadOSSame as iOS in compact; may appear in toolbar in regular width.
macOSTrailing toolbar search field. Always visible.
watchOSDictation-first input. Search bar at top of list.
tvOSTab-based search with on-screen keyboard.
SwiftUI搜索会根据平台自动适配:
平台默认行为
iOS搜索栏位于导航栏中。默认会随滚动隐藏;下拉可重新显示。
iPadOS紧凑布局下与iOS相同;常规宽度下可能显示在工具栏中。
macOS工具栏右侧的搜索栏。始终可见。
watchOS以语音输入优先。搜索栏位于列表顶部。
tvOS基于标签页的搜索,搭配屏幕键盘。

iOS-Specific Behavior

iOS特定行为

swift
// 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)
swift
// 始终可见的搜索栏(不会随滚动隐藏)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))

// 默认:搜索栏随滚动隐藏,下拉可显示
.searchable(text: $searchText)

macOS-Specific Behavior

macOS特定行为

swift
// Search in toolbar (default on macOS)
.searchable(text: $searchText, placement: .toolbar)

// Search in sidebar
.searchable(text: $searchText, placement: .sidebar)

swift
// 工具栏中的搜索栏(macOS默认)
.searchable(text: $searchText, placement: .toolbar)

// 侧边栏中的搜索栏
.searchable(text: $searchText, placement: .sidebar)

Part 9: Common Gotchas

第九部分:常见问题

1. Search Field Doesn't Appear

1. 搜索栏不显示

Cause:
.searchable
is not inside a navigation container.
swift
// WRONG: No navigation container
List { ... }
    .searchable(text: $query)

// CORRECT: Inside NavigationStack
NavigationStack {
    List { ... }
        .searchable(text: $query)
}
原因
.searchable
未放置在导航容器内。
swift
// 错误:没有导航容器
List { ... }
    .searchable(text: $query)

// 正确:在NavigationStack内
NavigationStack {
    List { ... }
        .searchable(text: $query)
}

2. isSearching Always Returns false

2. isSearching始终返回false

Cause: Reading
isSearching
from the wrong view level.
swift
// 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
swift
// 错误:从附加了.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()
        }
    }
}

3. Suggestions Don't Fill Search Field

3. 点击建议不会填入搜索栏

Cause: Missing
.searchCompletion()
on suggestion views.
swift
// 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()
修饰符。
swift
// 错误:没有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)  // 点击后填入搜索栏
    }
}

4. Placement on Wrong Navigation Level

4. 位置设置在错误的导航层级

Cause: Attaching
.searchable
to the wrong column in NavigationSplitView.
swift
// 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中,.searchable附加到了错误的列。
swift
// 可能不会显示在预期位置
NavigationSplitView {
    SidebarView()
} detail: {
    DetailView()
}
.searchable(text: $query)  // 由系统选择列

// 明确指定位置
NavigationSplitView {
    SidebarView()
        .searchable(text: $query, placement: .sidebar)  // 在侧边栏
} detail: {
    DetailView()
}

5. Search Scopes Don't Appear

5. 搜索范围不显示

Cause: Scopes require
.searchable
on the same view. They also require a navigation container.
swift
// 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
附加到同一个视图,且需要导航容器。
swift
// 错误:没有searchable的情况下添加scopes
List { ... }
    .searchScopes($scope) { ... }

// 正确:scopes与searchable一起使用
List { ... }
    .searchable(text: $query)
    .searchScopes($scope) {
        Text("All").tag(Scope.all)
        Text("Recent").tag(Scope.recent)
    }

6. iOS 26 Refinements

6. iOS 26优化功能

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.

关于底部对齐搜索、
.searchToolbarBehavior(.minimize)
Tab(role: .search)
DefaultToolbarItem(kind: .search)
,请查看
axiom-swiftui-26-ref
。这些功能基于本文档中的基础API构建。

Part 10: API Quick Reference

第十部分:API快速参考

Modifiers

修饰符

ModifieriOSPurpose
.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
修饰符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+处理搜索提交

Environment Values

环境值

ValueiOSPurpose
isSearching
15+Is user actively searching
dismissSearch
15+Action to dismiss search
iOS版本用途
isSearching
15+用户是否正在搜索
dismissSearch
15+关闭搜索的操作

Types

类型

TypeiOSPurpose
SearchFieldPlacement
15+Where search field renders
SearchScopeActivation
16.4+When scopes appear

类型iOS版本用途
SearchFieldPlacement
15+搜索栏的渲染位置
SearchScopeActivation
16.4+搜索范围的激活时机

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+
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+