SwiftUIでListを検索できるようにする方法

目次

概要

SwiftUIで、Listを使ってCoreDataに保存されている情報を表示しているのですが、その内容について検索をしたくなりました。

ここでは、CoreDataを利用したListで検索をする方法について紹介します。

環境

  • Xcode 13
  • SwiftUI 3
  • iOS 15.1

やりたいこと

最初に、どういうことをしたいのか書いておきます。

前提として、Listを使ってCoreDataの情報を表示しています。Listを使っているためソースコードは比較的単純なものとなっており、CoreDataからデータを取り出して、それを表示するだけとなっています。

これに対して、CoreDataに保存されている情報が多くなると、リストから情報を探すのが大変になりました。そこで、テキストフィールドに検索文字列を入力すると、その文字列を含むデータのみ表示するようにします。

また、検索文字列が入力されていないときは、全てのデータを表示します。

実装の方針

Listに表示するデータが格納されている変数の中身を変えるというのが、基本方針です。

しかし、ただ変数の中身を変えようとしても、あまりうまくいきません。うまく行く方法として、リスト表示の部分をべつのViewとして分けて、それを利用する形にします。このViewの引数として検索文字列を渡して、その文字列に応じてListの内容を変化させます。

わざわざViewを分けたくはなかったのですが、CoreDataを利用してデータを取り出す関係でこのような形になっています。

実装

IndexViewという名前のViewの中で、ListViewというViewを用意しています。

import SwiftUI

struct IndexView: View {
    @State var searchText: String = "" // 検索文字列

    var body: some View {
        VStack {
            // 検索文字列を入力するテキストフィールド
            TextField("検索", text: $searchText)
                .background(Color("TextBackground"))

            // 検索結果を表示するために別のViewを描画
            ListView(searchText: searchText)
        }
    }
}

struct IndexView_Previews: PreviewProvider {
    static var previews: some View {
        IndexView()
    }
}

// 検索結果のリスト
struct ListView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item> // 全てのデータ

    var fetchRequest: FetchRequest<Item> // 検索結果

    init(searchText: String) {
        // タイトルと説明の内容を検索
        fetchRequest = FetchRequest<Item>(
            entity: Item.entity(),
            sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
            predicate: NSPredicate(format: "title CONTAINS %@", searchText))
    }

    var body: some View {
        // 全てのデータを表示
        if (searchText.isEmpty) {
            List {
                ForEach(items) { item in
                    Text("文字列")
                }
            }
        }
        else {
            List {
                ForEach(fetchRequest.wrappedValue, id: \.self) { item in
                    Text("文字列")
                }
            }
        }
    }
}

ListViewでは、searchText変数として検索文字列を受け取り、その文字列をもとに、FetchRequestのpredictateで検索の設定を行っています。title CONTAINS %@は、titleカラムについてsearchText変数の文字列を含むものを全て取り出すという意味です。カラム名などは適宜変更してください。また、今回はItemテーブルからデータを取得しています。

ListViewでは、ifを用いて検索文字列が空かどうかでリストの表示を変更しています。これは、検索文字列が空の場合、CoreDataの検索結果が0件となってしまうためです。

これで、検索文字列によってリストの内容を変更できるようになりました。

さいごに

空文字列で検索をすると検索結果が0となるので、それについての処理に少し時間がかかりました。色々と調べてみても、CoreDataの検索の仕様がこのようになっているため、今回の方法を採用しました。