7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

iOSアプリの検索ビューにおけるタグ(UISearchToken)の追加

Last updated at Posted at 2022-01-31

iOSの検索ビュー内でタグを追加し、検索対象のカテゴリーを指定することができます。

このタグは検索トークンと呼ばれるものです。

ここでは、検索トークンを作成し、それを検索バーに追加する方法を説明します。 さらに、SwiftUIアプリに検索トークンを使った検索バーを加えることについても説明します。

サーチトークンの作成

UISearchTokenプロパティを使用し、サーチトークンを作成することができます。このオブジェクトは、UIImageアイコンやタイトルで初期化することができます。

Apple Developer Documentation

また、トークンの識別子を指定する必要があります(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)
    }
}

上の例では、ユーザーは、映画、歌、文書のいずれかの検索を選択できます。ユーザーが検索しているカテゴリーは、タグ(検索トークン)として表示されます。

完成したプロジェクト

完成したプロジェクトのコードをこちらからご覧いただけます。

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?