mcpField
A
axiom-swiftui-search-ref

axiom-swiftui-search-ref

github.com/charleswiltgen开发1.2k152 次安装今天更新
代码仓库

关于 axiom-swiftui-search-ref

面向 SwiftUI 开发者的搜索 API 参考技能,涵盖不同系统版本的搜索行为、配置与常见问题,重点说明搜索字段与导航容器的关系。

主要功能

  1. 01searchable API 与位置配置
  2. 02搜索建议与自动补全
  3. 03搜索范围与结构化令牌
  4. 04搜索焦点控制与提交
  5. 05平台行为与问题排查

适用场景

  1. 01为 SwiftUI 列表添加搜索
  2. 02筛选结果或提交查询
  3. 03构建范围或令牌搜索
  4. 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)
17isPresented 参数、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+

工作原理
  1. 将 .searchable(text: $query) 附加到视图
  2. 最近的导航容器(NavigationStack、NavigationSplitView)会呈现搜索字段
  3. 视图通过环境接收 isSearching 和 dismissSearch
  4. 视图会根据绑定的文本筛选或查询
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用途
isSearching15+用户是否正在主动搜索
dismissSearch15+关闭搜索的操作
类型
类型iOS用途
SearchFieldPlacement15+搜索字段的显示位置
SearchScopeActivation16.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+

评论

支持 Markdown