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. 01SwiftUIのリストに検索を追加
  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 になります。

重要なルール:isSearching は、.searchable が付いたビューの子から読み取る必要があります。SwiftUI は 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
        }
    }
}
dismissSearch 環境値
@Environment(\.dismissSearch) private var dismissSearch

対応バージョン:iOS 15 以降

dismissSearch() を呼び出すと、検索テキストが消去され、フォーカスが解除され、isSearching が false に設定されます。searchable ビュー階層内から呼び出す必要があります。

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+

コメント