Closed7

[SwiftUI]検索バー実装したい我

ほへとほへと

これを読む。

https://developer.apple.com/documentation/swiftui/adding-a-search-interface-to-your-app


概要

検索可能なビュー修飾子の 1 つ (searchable(text:placement:prompt:) など) を NavigationSplitView または NavigationStack、あるいはこれらのいずれかの内部のビューに適用して、アプリに検索インターフェースを追加します。

struct ContentView: View {
    @State private var departmentId: Department.ID?
    @State private var productId: Product.ID?
    @State private var searchText: String = ""

    var body: some View {
        NavigationSplitView {
            DepartmentList(departmentId: $departmentId)
        } content: {
            ProductList(departmentId: departmentId, productId: $productId)
        } detail: {
            ProductDetails(productId: productId)
        }
        .searchable(text: $searchText) // Adds a search field.
    }
}

NavigationSplitViewであれば、配置場所の設定ができる。

NavigationSplitView {
    DepartmentList(departmentId: $departmentId)
} content: {
    ProductList(departmentId: departmentId, productId: $productId)
} detail: {
    ProductDetails(productId: productId)
}
.searchable(text: $searchText, placement: .sidebar)

Placeholderを変更することができる。
デフォルトは"Search"

DepartmentList(departmentId: $departmentId)
    .searchable(text: $searchText, prompt: "Departments and products")

NavigationLinkや、NavigationStackに付与しないと使えないみたい。

ほへとほへと

検索文字に変更があった場合に、処理をしたい。
onChangeを使用することで、変更時に処理ができるらしい。

struct SearchView: View {
    @State private var searchText: String = ""
    
    var body: some View {
        NavigationStack {
            List {
                SearchViewCell(
                    title: "Swift",
                    description: "description"
                )
            }
            .searchable(text: $searchText, prompt: "検索")
            .onChange(of: searchText, {
                // 検索処理
            })
        }
    }
}
ほへとほへと

onChangeについては以下を参照する。

https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-8wgw9


概要

特定の値が変更されたときにアクションを実行する、このビューの修飾子を追加します。

nonisolated
func onChange<V>(
    of value: V,
    initial: Bool = false,
    _ action: @escaping () -> Void
) -> some View where V : Equatable

valueEquatableを継承している必要がある。
initialは、このViewが最初に実行された時に処理を実行するかどうかを決める。

システムはメインアクターでアクション クロージャを呼び出す可能性があるため、クロージャ内で長時間実行されるタスクは避けてください。このようなタスクを実行する必要がある場合は、非同期のバックグラウンド タスクをデタッチします。

struct PlayerView: View {
    var episode: Episode
    @State private var playState: PlayState = .paused

    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(playState: $playState)
        }
        .onChange(of: playState) {
            model.playStateDidChange(state: playState)
        }
    }
}
ほへとほへと

検索ワードを提案する機能があるとのこと。
以下を見ていく。

https://swappli.com/searchsuggestions/


searchSuggestionsについて

以下は検索フィールドをタップしたら、検索候補が一覧となって表示される。

import SwiftUI

struct searchSuggestions: View {
    @State private var searchText = ""

    var body: some View {
        NavigationStack {
            Text("Hello World!")
                .frame(width: 400, height: 500)
                .navigationTitle("フルーツ")
                .searchable(text: $searchText)
                .searchSuggestions {
                    Text("🍎 Apple").searchCompletion("apple")
                    Text("🍌 Banana").searchCompletion("banana")
                    Text("🍊 Orange").searchCompletion("orange")
                    Text("🍇 Grapes").searchCompletion("grapes")
                    Text("🍓 Strawberry").searchCompletion("strawberry")
                }
            Text("選択したフルーツ:" + searchText)
        }
    }
}

SearchCompletionについて

SearchCompletionについて気になったので、以下を参照する。
https://developer.apple.com/documentation/swiftui/view/searchcompletion(_:)-e0pr

検索候補として使用される場合、検索トークンをこのビューの値に関連付けます。

nonisolated
func searchCompletion<T>(_ token: T) -> some View where T : Identifiable

token
ビューの補完として使用するデータ。

このメソッドを使用して、検索候補リストのコンテキスト内にあるビューに検索トークンを関連付けます。 ビューが選択されると、システムはこの値を使用して、関連付けられた検索フィールドの現在編集中の部分テキストを置き換えます。

SearchCompletionの設定は、タップしたViewの値をSearchBarに入力されるために設定が必須。
これが設定されていないと、Viewに対して紐づく値がないので、SearchBarの変化はない。

Tokenを活用して、検索フィールドにViewを関連付ける

複数のトークンが入るよう、配列を用意する。
トークンに応じて、検索フィールドにTextが入るようになる。

import SwiftUI

enum FruitToken: Hashable, Identifiable, CaseIterable {
    case apple
    case pear
    case banana

    var id: Self { self }
}

struct SearchView: View {
    @State private var searchText: String = ""
    // 選択済みのTokenを格納する配列
    @State private var tokens: [FruitToken] = []
    
    var body: some View {
        NavigationStack {
            List {
                SearchViewCell(
                    title: "Swift",
                    description: "description"
                )
            }
            .searchable(text: $searchText, tokens: $tokens, prompt: "検索") { token in
                switch token {
                case .apple: Text("Apple")
                case .pear: Text("Pear")
                case .banana: Text("Banana")
                }
            }
            .searchSuggestions {
                Text("🍎 Apple").searchCompletion(FruitToken.apple)
                Text("🍐 Pear").searchCompletion(FruitToken.pear)
                Text("🍌 Banana").searchCompletion(FruitToken.banana)
            }
        }
    }
}

searchSuggestionsを別の方法で指定する

先程とは振る舞いが以下のように異なる
・選択が一つのみ
・検索候補リストには、SwichされたTextViewが表示される

import SwiftUI

enum FruitToken: Hashable, Identifiable, CaseIterable {
    case apple
    case pear
    case banana

    var id: Self { self }
}

struct SearchView: View {
    @State private var searchText: String = ""
    @State private var tokens: [FruitToken] = []
    @State private var suggestions: [FruitToken] = FruitToken.allCases
    
    var body: some View {
        NavigationStack {
            List {
                SearchViewCell(
                    title: "Swift",
                    description: "description"
                )
            }
            .searchable(
                text: $searchText,
                tokens: $tokens,
                suggestedTokens: $suggestions,
                prompt: "検索"
            ) { token in
                switch token {
                case .apple: Text("Apple")
                case .pear: Text("Pear")
                case .banana: Text("Banana")
                }
            }
        }
    }
}

検索候補を動的に変える実装もしていて面白い。

ほへとほへと

公式の内容も読んでおく。

https://developer.apple.com/documentation/swiftui/performing-a-search-operation


概要

保存した検索テキストとオプションのトークンに基づいて検索結果を更新します。

検索結果を複数のView間で共有する例

struct ContentView: View {
    @EnvironmentObject private var model: Model
    @State private var departmentId: Department.ID?
    @State private var productId: Product.ID?

    var body: some View {
        NavigationSplitView {
            DepartmentList(departmentId: $departmentId)
        } content: {
            ProductList(departmentId: departmentId, productId: $productId)
                .searchable(text: $model.searchText)
        } detail: {
            ProductDetails(productId: productId)
        }
    }
}

トークンを作成するには、Identifiable プロトコルに準拠する値のグループを定義し、値のコレクションをインスタンス化します。たとえば、フルーツ トークンの列挙を作成できます。

ほへとほへと

searchable(text:tokens:placement:prompt:token:)について公式見てみる。

https://developer.apple.com/documentation/swiftui/view/searchable(text:tokens:placement:prompt:token:)-3gbeu


概要

このビューをテキストとトークンで検索可能としてマークします。

nonisolated
func searchable<C, T, S>(
    text: Binding<String>,
    tokens: Binding<C>,
    placement: SearchFieldPlacement = .automatic,
    prompt: S,
    @ViewBuilder token: @escaping (C.Element) -> T
) -> some View where C : RandomAccessCollection, C : RangeReplaceableCollection, T : View, S : StringProtocol, C.Element : Identifiable

token:
検索フィールドで表示および編集するトークンのコレクション。
token:
トークン内の要素を指定してビューを作成するビュー ビルダー。

tokenは、@ViewBuilderが付与されていて、Tokenに応じてTextViewなど設定することができる。
SuggestionするときのViewはここで作られているのかな。

ほへとほへと

まとめ

NavigationStackとかを使用してないと使えない
.searchable()を使用する
onChangeを使用して変更時の処理を記述できる
・検索補完機能が使える

このスクラップは5ヶ月前にクローズされました