SwiftDataで動的なキーワード検索を実装する

最近は自分向けのPDFリーダーアプリを開発しています。
このアプリではSwiftDataを使って読書履歴をモデル化して管理しているのですが、これを踏まえてちょっと自分のメモがてらにTODOアプリを例にしたメモを残しておきます。

Swiftを触り始めて3日目ぐらいなので、書き間違えや嘘などがあるかもしれません。
その場合はコメントなどで教えてもらえると助かります。

TL;DR

  • SwiftDataの@Queryで動的な条件は付与できない
  • Computed Propertiesを使って@Queryで定義したフィルターを利用する

基本的な使い方

まず今回のサンプルとなるタスクを一覧表示するViewと、Modelを定義します。
以下のような仕様で作成しておきます。

  • Taskモデル
    • nameをプロパティとして持つ
  • TodoView
    • Taskを一覧表示する
    • 画面上部にはキーワード検索のためのTextFieldを設定
import SwiftUI
import SwiftData

@Model
final class Task {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

struct TodoView: View {
    @Environment(\.modelContext) private var context
    @State private var keyword: String = ""
    @Query(sort: \Task.name) private var tasks: [Task]

    var body: some View {
        VStack {
            HStack {
                TextField("検索...", text: $keyword)
                    .padding()
                    .background(primaryColor)
                    .cornerRadius(5)
            }
            .buttonStyle(PlainButtonStyle())
            .padding(.horizontal)
            
            List(tasks) { task in
                Text(task.name)
            }
        }
    }
}

// Preview
struct TodoView_Previews: PreviewProvider {
    static var words: [String] = [
        "Summery", "Country", "Perk", "Relief", "Virus",
        "Hunter", "Photocopy", "Liberal", "Sugar", "Practice",
    ]

    static var container: some ModelContainer {
        let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try! ModelContainer(for: Task.self, configurations: configuration)
        
        // プレビュー用データを作成
        words.forEach { word in
            let task = Task(name: word)
            container.mainContext.insert(task)
        }
        
        return container
    }
    
    static var previews: some View {
        TodoView()
            .modelContainer(container)
    }
}

この段階では、検索用のTextFieldがありますが絞り込むロジックを書いていません。
そのため絞り込むためのロジックを書いていきます。

キーワードでタスクを絞り込む 

キーワード検索を絞り込むためにはどういったやり方があるでしょうか?
最初は@Queryのfilterに絞り込むためのロジックを書けばいけると思っていたのですが、ここではkeyword自体を参照することができませんでした。

// エラー
@Query(filter: #Predicate<Task> { $0.name.contains(keyword) }, sort: \Task.name) private var tasks: [Task]

そして小一時間ほど公式ドキュメントを眺めたりしながら試行錯誤した結果、Computed Propertiesなプロパティを定義することで実現することができました。

// 成功
@Query(sort: \Task.name) private var tasks: [Task]
private var filteredTasks: [Task] {
    guard !keyword.isEmpty else { return tasks }
    return tasks.filter { $0.name.contains(keyword) }
}
  • キーワードが空文字列の場合はtasksをそのまま返す
  • それ以外の場合、タスク名にキーワードが含まれている文字列を返す

というプロパティを定義することで動的なキーワード検索を比較的楽に実現することができました。

もし同じように悩んでいる方がいれば参考にしてみてください。