iOSの検索ビュー内でタグを追加し、検索対象のカテゴリーを指定することができます。
このタグは検索トークンと呼ばれるものです。
ここでは、検索トークンを作成し、それを検索バーに追加する方法を説明します。 さらに、SwiftUIアプリに検索トークンを使った検索バーを加えることについても説明します。
サーチトークンの作成
UISearchToken
プロパティを使用し、サーチトークンを作成することができます。このオブジェクトは、UIImage
アイコンやタイトルで初期化することができます。
また、トークンの識別子を指定する必要があります(representedObject
プロパティを使用)。この変数はどのような型でもよく、コードで自分が参照するために使います。一般的に、この変数には文字列の識別子を設定します。
例えば、ユーザーがSwiftUI
についての記事を検索している場合、次のようにUISearchToken
オブジェクトを初期化することができます:
var token = UISearchToken(icon: UIImage(systemName: "swift"), text: "SwiftUI")
token.representedObject = "#swiftui"
識別子は検索トークンから後で読み取ることができます。
if let identifier = token.representedObject as? String {
print(identifier)
}
検索トークンを検索バーに追加する
UISearchController
では、検索トークンを設定できます。
searchController.searchBar.searchTextField.tokens = searchTokens
また、検索トークンを追加または削除したり、テキスト内にあるトークンの位置を取得したり、指定されたテキスト位置の間にある全トークンを取得したりするなどの追加機能もあります。
open var tokens: [UISearchToken]
open func insertToken(_ token: UISearchToken, at tokenIndex: Int)
open func removeToken(at tokenIndex: Int)
open func positionOfToken(at tokenIndex: Int) -> UITextPosition
open func tokens(in textRange: UITextRange) -> [UISearchToken]
UIKitアプリに検索トークンを追加する
UIKitアプリに検索トークンを追加するのは簡単です。UISearchController
を作成し、searchController.searchBar.searchTextField.tokens
変数を使用して検索トークンをそのビューコントローラに割り当てるだけです。
SwiftUIアプリに検索トークンを追加する
SwiftUIには、新たな.searchable
ビューモディファイアがあります。しかし、このモディファイアは検索トークンの追加をサポートしていません。そのため、UIViewControllerRepresentable
構造を実装する必要があります。
UIKitコンポーネントをSwiftUIのナビゲーションバーに追加する
検索バー(UIKitコンポーネント)を単に表示するだけではなく、SwiftUIのナビゲーションバーに直接追加したいと思います。
class NavBarEmbeddedSearch: UIViewController {
let searchController = UISearchController()
override func viewDidLoad() {
searchController.hidesNavigationBarDuringPresentation = true
searchController.obscuresBackgroundDuringPresentation = false
}
override func viewWillAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
}
viewDidLoad
関数内では、検索バーのUIに関する基本的な設定を行います。
このビューコントローラが画面に表示されるとき(viewWillAppear
が呼び出されるとき)、検索コントローラが親ナビゲーションバー(SwiftUIが管理するもの)に割り当てられます。
SearchBar(サーチバー)構造体の作成
次に、SearchBar
の基本構造を作成します。これはタイプUIViewControllerRepresentable
に準拠するもので、UIKitをSwiftUIに移植する(利用できるようにする)ことを意味します。
fileprivate struct SearchBar: UIViewControllerRepresentable {
@Binding var searchText: String
@Binding var searchTokens: [UISearchToken]
init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) {
self._searchText = searchText
self._searchTokens = searchTokens
}
func makeUIViewController(context: Context) -> NavBarEmbeddedSearch {
// TODO 1
}
func updateUIViewController(_ controller: NavBarEmbeddedSearch, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(searchText: $searchText, searchTokens: $searchTokens)
}
class Coordinator: NSObject, UISearchResultsUpdating {
@Binding var searchText: String
@Binding var searchTokens: [UISearchToken]
init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) {
self._searchText = searchText
self._searchTokens = searchTokens
}
func updateSearchResults(for searchController: UISearchController) {
// TODO 2
}
}
}
上記のコードでご覧のように、SearchBar
構造体には検索テキストのバインディング変数と検索トークンが格納されることになります。
また、デリゲートとして動作するCoordinator
クラスもあり、検索バーから更新情報を受け取ります(ユーザーが検索テキストを入力したときやユーザーが検索トークンを削除したときなど)。
検索バーの初期化
上記のコードの// TODO 1
で、検索コントローラーの初期化を行います。
func makeUIViewController(context: Context) -> NavBarEmbeddedSearch {
let controller = NavBarEmbeddedSearch()
controller.searchController.searchBar.searchTextField.tokens = searchTokens
controller.searchController.searchResultsUpdater = context.coordinator
return controller
}
ここでは、サーチトークンに提供された値を設定します。
また、検索結果アップデータsearchResultsUpdater
変数には、作成したCoordinator
を設定します。これにより、検索文字が変更されたことがわかるようになります。
検索文字の変更でSwiftUIのバインディング変数を更新する
検索文字に変更があった場合、Coordinator
オブジェクト内のfunc updateSearchResults(for searchController: UISearchController)
関数が呼び出されます。
上記のコードの// TODO 2
コードブロックの中で、更新された検索文字でSwiftUIのバインディング変数を更新します。
guard let text = searchController.searchBar.text else { return }
self.searchText = text
self.searchTokens = searchController.searchBar.searchTextField.tokens
完成したコード
こちらが、完成したSearchBar
のコードです
class NavBarEmbeddedSearch: UIViewController {
let searchController = UISearchController()
override func viewDidLoad() {
searchController.hidesNavigationBarDuringPresentation = true
searchController.obscuresBackgroundDuringPresentation = false
}
override func viewWillAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
}
fileprivate struct SearchBar: UIViewControllerRepresentable {
@Binding var searchText: String
@Binding var searchTokens: [UISearchToken]
init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) {
self._searchText = searchText
self._searchTokens = searchTokens
}
func makeUIViewController(context: Context) -> NavBarEmbeddedSearch {
let controller = NavBarEmbeddedSearch()
controller.searchController.searchResultsUpdater = context.coordinator
controller.searchController.searchBar.searchTextField.tokens = searchTokens
return controller
}
func updateUIViewController(_ controller: NavBarEmbeddedSearch, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(searchText: $searchText, searchTokens: $searchTokens)
}
class Coordinator: NSObject, UISearchResultsUpdating {
@Binding var searchText: String
@Binding var searchTokens: [UISearchToken]
init(searchText: Binding<String>, searchTokens: Binding<[UISearchToken]>) {
self._searchText = searchText
self._searchTokens = searchTokens
}
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
self.searchText = text
self.searchTokens = searchController.searchBar.searchTextField.tokens
}
}
}
SwiftUIビュー内に検索バーを設置する
ここでは、SwiftUIビューのナビゲーションバーに検索バーを追加します。
まずは、SwiftUIビューがナビゲーションビュー内に埋め込まれていることを確認します。
検索コントローラーは、ナビゲーションバーにプログラムで直接割り当てるため、検索バーをビューコンポーネントにする必要はありません。従って、overlay
ビューモディファイアを利用することで、そのフレームを0に設定します。
struct ContentView: View {
/// Variables used by `SearchView`
@State private var searchText: String = ""
@State private var searchTokens: [UISearchToken]
init(searchTokens: [UISearchToken]) {
self._searchTokens = .init(initialValue: searchTokens)
}
var body: some View {
Form {
Section {
Text("Search term: \(searchText)")
Text("Search tokens: \(searchTokens.getTokenNames().description)")
}
}.overlay(
SearchBar(searchText: $searchText, searchTokens: $searchTokens).frame(width: 0, height: 0)
)
}
}
上の例では、入力された検索テキストとトークンを表示しているだけです。ご自分のアプリケーションでは、検索ワードが変わるたびに検索を実行する必要があります。
上記のSwiftUIビューを使う
これで、上記のSwiftUIビューを使って、タグと共に検索バーを表示できるようになりました。
struct MenuView: View {
var body: some View {
List {
NavigationLink("Movies") {
ContentView(searchTokens: [.init(icon: UIImage(systemName: "play.rectangle.on.rectangle.fill"), text: "Movies").getTokenWithIdentifier("movies")])
}
NavigationLink("Music") {
ContentView(searchTokens: [.init(icon: UIImage(systemName: "music.note"), text: "Music").getTokenWithIdentifier("music")])
}
NavigationLink("Documents") {
ContentView(searchTokens: [.init(icon: UIImage(systemName: "doc"), text: "Documents").getTokenWithIdentifier("documents")])
}
}
.navigationTitle("Search demo")
.navigationBarTitleDisplayMode(.inline)
}
}
上の例では、ユーザーは、映画、歌、文書のいずれかの検索を選択できます。ユーザーが検索しているカテゴリーは、タグ(検索トークン)として表示されます。
完成したプロジェクト
完成したプロジェクトのコードをこちらからご覧いただけます。