スポンサーリンク

[SwiftUI]CoreDataを複数条件で検索し表示する

スポンサーリンク

CoreDataを利用したサンプルアプリ第4段。

今回は、すでにCoreDataにデータが保存された状態から、複数条件で検索して絞込み、その結果で表示を切り替える方法です。
ユーザーが条件を選択することで、表示されるデータが動的に切り替わるように検索機能を作ります。

このサンプルアプリを絞込み検索できるようにさらに改造します。
CoreDataの取得・表示ができる状態を前提としてのサンプルコードとなります。

開発環境バージョン
Xcode14.0.1
macOSMonterey12.6

iOS15以降に対応のコードになってます。

スポンサーリンク

CoreDataを複数条件で絞り込み表示する

サンプルアプリでCoreDataに保存されるデータで絞込みに使うのは、「テキスト・日付・お気に入り」です。
つまりデータとしては「String, Date, Bool」となります。

NSPredicateとNSSortDiscriptorを使って絞り込めるようにします。

CoreDataを複数条件で絞込み表示するサンプルアプリの全容コード

CoreDataの複数条件検索 完成形画像

画面下部が絞込み条件の入力・選択フォームです。

ContentView.swift

今回はContentView.Swiftしか触りません。

//
//  ContentView.swift
//  SampleApp
//
//  Created by yaguchisato on 2021/11/26.
// メインView

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @StateObject private var sampleModel = SampleModel()
 //CoreDataのデータを日付降順で取得
   @FetchRequest(
        entity:SampleData.entity(),sortDescriptors: [NSSortDescriptor(keyPath: \SampleData.date, ascending: false)],
        animation: .default)
    private var samples: FetchedResults<SampleData>

/*絞込み用の変数・関数を設定*/
//検索ワードを入れる変数
    @State private var textSearch: String = ""
//お気に入りかどうかを選択する変数
    @State private var favoritesOnly = false
//日付の並び順を並び替える変数
    @State private var dateOrder: Bool = false
    
//呼び出したら絞込み条件を切り替えてくれる便利な関数
    private func searchFilter(searchText: String, favorite: Bool, dateSort: Bool){
//お気に入りのみ表示
        let favPredicate: NSPredicate = NSPredicate(format: "bool = %@", NSNumber(value: true))
//文字検索と一致するものを表示
        let textPredicate: NSPredicate = NSPredicate(format: "text contains[c] %@", searchText)
//お気に入りと文字検索を表示
        let favTextPredicate: NSPredicate = NSPredicate(format: "bool = %@ and text contains[c] %@", NSNumber(value: true),searchText)
//日付の降順昇順ソート
        let dateSorted: NSSortDescriptor = NSSortDescriptor(key: "date", ascending: dateSort ? true : false)
  
//お気に入りかどうか、検索窓が空かどうかで表示を切替
        switch(favorite, searchText.isEmpty){
//お気に入りで検索窓は入力無
        case(true, true):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = favPredicate
//お気に入りチェック有、検索窓に入力有
        case(true, false):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = favTextPredicate
//お気に入りチェック無、検索窓に入力有
        case(false, false):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = textPredicate
//絞込み条件なし
        default:
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = nil
            
        }
    }
 

    var body: some View {
        VStack(alignment: .center){
            //新規作成ボタン
            Button(action:
                    {
                sampleModel.isNewData.toggle()
                print(sampleModel.isNewData)
            }){
                Text("新規作成はここをクリック")
            }
            //タップするとシートが開く
            .sheet(isPresented: $sampleModel.isNewData,
                   content: {
                SheetView(sampleModel: sampleModel)
            })
            
            
            //データを表示する
            List{
                ForEach(samples){samples in
                    SampleCardVIew(sampleModel: sampleModel, samples: samples)
                }
            }
            
            
 /*以下絞込み条件入力フォーム*/
            VStack{
            //検索窓
            HStack {
                TextField("文字検索",text: $textSearch)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .onSubmit{
                        searchFilter(searchText: textSearch, favorite: favoritesOnly, dateSort: dateOrder)
                    }
                //入力を始めたら現れる入力文字一括クリアボタン
                if !textSearch.isEmpty {
                    Button(action: {
                        textSearch = ""
                    }){
                        Image(systemName: "delete.left.fill")
                            .resizable()
                            .scaledToFit()
                            .foregroundColor(.gray)
                            .frame(width: 30, height: 30)
                    }
                }
            }
            
            //お気に入りで絞り込む用のチェックボタン
              Toggle(isOn: $favoritesOnly){
                    Text("お気に入り")
                        .font(.body)
                        .foregroundColor(.primary)
               }.tint(.orange)
                .onChange(of: favoritesOnly){new in
                searchFilter(searchText: textSearch, favorite: favoritesOnly, dateSort: dateOrder)
                }
            //日付の並び順ソート
            HStack{
                Text("日付順")
                    .font(.body)
                    .foregroundColor(.primary)
                Spacer()
                Button(action: {
                    dateOrder.toggle()
                    searchFilter(searchText: textSearch, favorite: favoritesOnly, dateSort: dateOrder)
                }){
                    Text(dateOrder ? "昇順" : "降順")
                }

            }
        }
            .padding(.horizontal,50)

}
}
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

大まかにまとめると、

  • 絞込みのための条件を入力できる変数とボタン等を用意
  • 入力内容によってCoreDataの絞込み条件を切り替える関数を作る
  • 条件入力時に関数が呼び出されるようにする

という流れです。そんなに複雑なことはありません。
以下、順を追って解説します。

CoreDataを絞込みできる文字検索窓

まずは検索窓を作りましょう。
今回はTextfieldで自作します。

1.TextFieldで使う変数を設定

TextFieldに入力された文字を格納する変数textSearchを設定。

@State private var textSearch: String = ""

2.TextFieldを作る

1の変数textSearchを使います。

TextField("文字検索",text: $textSearch)
                    .textFieldStyle(RoundedBorderTextFieldStyle())

3.(おまけ)入力文字一括クリアボタン

TextFieldに文字が入力され始めたら右端に現れる、一括クリアできるボタンです。
あると便利なのでおまけ。
HStackでTextFieldと一緒に括っておきます。

 if !textSearch.isEmpty {
                    Button(action: {
                        textSearch = ""
                    }){
                        Image(systemName: "delete.left.fill")
                            .resizable()
                            .scaledToFit()
                            .foregroundColor(.gray)
                            .frame(width: 30, height: 30)
                    }
   }

CoreDataを絞込みできるお気に入りボタン

次に、お気に入り登録したデータを絞り込むためのボタンを作ります。

1.お気に入りを切り替えるための変数を設定

お気に入りのデータだけを表示するかどうか、を切り替えるためのBool変数favoritesOnlyを設定します。

@State private var favoritesOnly = false

2.お気に入りを切り替えるボタンを作る

ここはToggleボタンにしてみました。
Boolを切り替えられるならButtonでもなんでもいいと思います。

 Toggle(isOn: $favoritesOnly){
                    Text("お気に入り")
                        .font(.body)
                        .foregroundColor(.primary)
  }.tint(.orange)

.tintでトグルボタンの色が変えられます。

CoreDataを日付でソートするボタン

次に、日付の昇順と降順を切り替えて並び替えるボタンを作ります。

1.日付をソートするための変数を設定

日付のソート順を切り替える変数dateOrderを設定します。

@State private var dateOrder: Bool = false

2.日付をソートするためのボタンを作る

今度はButtonです。
タップすると変数dateOrderがtoggleして表示される文字も切り替わります。

 Button(action: {
               dateOrder.toggle()
               searchFilter(searchText: textSearch, favorite: favoritesOnly, dateSort: dateOrder)
  }){
               Text(dateOrder ? "昇順" : "降順")
 }

CoreDataを複数条件で絞込みする関数

文字入力とお気に入りという複数条件で絞込み表示、日付順で並び替えしたい。
呼び出せば絞込み条件を切り替えてViewも切り替わる、便利な関数SearchFilterを作ります。

CoreDataを絞り込む関数を作る

    private func searchFilter(searchText: String,favorite: Bool,dateSort: Bool){

//お気に入りのみ表示
        let favPredicate: NSPredicate = NSPredicate(format: "bool = %@", NSNumber(value: true))

//文字検索と一致するものを表示
        let textPredicate: NSPredicate = NSPredicate(format: "text contains[c] %@", searchText)

//お気に入りと文字検索を表示
        let favTextPredicate: NSPredicate = NSPredicate(format: "bool = %@ and text contains[c] %@", NSNumber(value: true),searchText)

//日付の降順昇順ソート
        let dateSorted: NSSortDescriptor = NSSortDescriptor(key: "date", ascending: dateSort ? true : false)
  
//お気に入りかどうか、検索窓が空かどうかで表示を切替
        switch(favorite,searchText.isEmpty){
//お気に入りで検索窓は入力無
        case(true, true):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = favPredicate
//お気に入りチェック有、検索窓に入力有
        case(true, false):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = favTextPredicate
//お気に入りチェック無、検索窓に入力有
        case(false, false):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = textPredicate
//絞込み条件なし
        default:
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = nil
            
        }
    }

呼び出した時の入力内容によって絞込み条件を切り替えてくれる関数です。

引数を設定しておいて、絞込み条件を格納した変数を受け取るようにします。以下詳細。

NSPredicateで絞込みの条件設定

CoreDataの絞込みにはNSPredicateを利用できます。
NSPredicate(format: “CoreDataのAttribute = %@”, 比較したい値)
という形式で条件を設定します。

今回のサンプルではテキスト(text)とお気に入り(bool)がCoreDataのAttributeです。

CoreDataのテキスト(text)のうち検索語句searchTextを含むものを抽出
let textPredicate: NSPredicate = NSPredicate(format: "text contains[c] %@", searchText)
CoreDataのお気に入り(bool)がtrueに等しいものを抽出
 let favPredicate: NSPredicate = NSPredicate(format: "bool = %@", NSNumber(value: true))
CoreDataの
textが検索語句searchTextを含む かつ boolがtrue
複数条件で抽出
let favTextPredicate: NSPredicate = NSPredicate(format: "bool = %@ and text contains[c] %@", NSNumber(value: true),searchText)

NSSortDescriptorで並び順の条件設定

CoreDataの並び順ではNSSortDescriptorを利用できます。
NSSortDescriptor(key: “CoreDataのAttribute”, ascending: bool値)
という形式で条件を設定します。

CoreDataの日付で昇順降順の並び替えをする

日付の並び順を切り替えるための変数も用意してあるので、その値(dateSort)によって並び順が切り替えられるよう三項演算子で設定します。

let dateSorted: NSSortDescriptor = NSSortDescriptor(key: "date", ascending: dateSort ? true : false)

switch構文で絞込み条件を切り替える

あとは、設定したフィルタリングとソートの定数たちを、関数の引数を使って切り替えます。
これはswitchで切り替えていますが、if文など他に良い切り替え方法があるかもしれません。

//お気に入りかどうか、検索窓が空かどうかで表示を切替
        switch(favorite,searchText.isEmpty){
//お気に入りで検索窓は入力無
        case(true, true):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = favPredicate
//お気に入りチェック有、検索窓に入力有
        case(true, false):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = favTextPredicate
//お気に入りチェック無、検索窓に入力有
        case(false, false):
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = textPredicate
//絞込み条件なし
        default:
            samples.nsSortDescriptors = [dateSorted]
            samples.nsPredicate = nil
            
        }

NSPredicateとNSSortdescriptorsは並べて記入してもちゃんと動作するようです。

作ったフィルタリング用関数を呼び出す

関数ができたので、適切なタイミングで呼び出しましょう。

1.TextFieldに入力され時に関数searchFilterを呼び出す

TextFieldに.onSubmitを追加して、入力完了時にsearchFilter関数を呼び出します。

TextField("文字検索",text: $textSearch)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .onSubmit{
                        searchFilter(searchText: textSearch, favorite: favoritesOnly, dateSort: dateOrder)
                    }

.onSubmitはtextFieldで改行ボタンが押されたら発動します。
呼び出し時にtextFieldの入力内容(textSearch)を関数の引数(searchText)に渡しています。

2.お気に入りボタンがトグルされたら関数searchFilterを呼び出す

.onChangeでfacoritesOnlyの値を監視して、変更されたらsearchFilter関数を呼び出します。

Toggle(isOn: $favoritesOnly){
         Text("お気に入り")
                        .font(.body)
                        .foregroundColor(.primary)
}
.tint(.orange)
.onChange(of: favoritesOnly){new in
          searchFilter(searchText: textSearch, favorite: favoritesOnly, dateSort: dateOrder)
 }

呼び出し時にfavoritesOnlyを関数の引数(favorite)に渡しています。

3.日付ソートボタンが押されたら関数searchFilterを呼び出す

Buttonが押されたらsearchFilter関数を呼び出します。

 Button(action: {
              dateOrder.toggle()
              searchFilter(searchText: textSearch, favorite: favoritesOnly, dateSort: dateOrder)
}){
              Text(dateOrder ? "昇順" : "降順")
 }

NSPredicateで複数条件の記述方法まとめ

前述の通りCoreDataの絞込みにはNSPredicateを利用できます。
NSPredicate(format: “CoreDataのAttribute = %@”, 比較したい値)
という形式で条件を設定します。

contains、likeなどの構文は検索すれば出てくるのですが、複数条件の記述方法がなかなかわかりにくかったので、最後に少しだけ触れておきます。

and検索

いくつかの条件があり、すべてに当てはまる「かつ」で検索したい場合。

boolがtrueでtextに「abc」が含まれる場合

let samplePredicate = NSPredicate(format: "bool = %@ and text contains[c] %@", NSNumber(value: true),"abc")

contains[c]の[c]は大文字・小文字を区別したくない場合につけます。

or検索

いくつか条件があり、いずれかに当てはまる「または」で検索したい場合。

textに「abc」が含まれる または memoが「abc」に等しい場合

let samplePredicate = NSPredicate(format: "text contains[c] %@ or memo = %@", "abc","abc")

and検索とor検索を併用したい

いくつか条件があり、「かつ」と「または」が入り混じる場合。

let samplePredicate = NSPredicate(format: "bool = %@ and (text contains[c] %@ or memo = %@)", NSNumber(value: true),"abc","abc")

and検索とor検索を併用したい時は、()で括ればOKです。
記述方法がデータ型によって変わるので注意が必要ですが、String、BoolだけでなくInt、Date等他のデータ型ももちろん検索できます。

入力&選択条件でViewは自動的に切り替わる

CoreDataに保存された値が何かによって絞込みや検索の内容が変わると思いますが、今回の応用で複数条件の絞込みができると思います。
関数さえ作ってしまえばあとは適切な場所で呼び出すだけで勝手にViewを更新できるので、大変便利です。
検索用画面のViewファイルは分けるのが実用的かとは思いますが、今回はサンプルなのでこんな感じになりました。

以上です。長い。お疲れ様でした!

タイトルとURLをコピーしました