axiom-swiftui-search-ref について
SwiftUIで検索機能を実装・デバッグする開発者向けのAPIリファレンスです。検索候補、スコープ、トークン、フォーカス制御に加え、ナビゲーションコンテナに関する注意点も扱います。
主な機能
- 01searchable APIと配置設定
- 02検索候補と補完
- 03スコープと構造化トークン
- 04検索フォーカスと送信
- 05プラットフォーム動作と問題診断
活用シーン
- 01SwiftUIのリストに検索を追加
- 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 になります。
重要なルール: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 | 用途 |
|---|---|---|
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+