👀
【SwiftUI × CoreData】検索絞り込みを実装する
前提
先日SwiftUI × CoreDataを使ってCRUD機能を持つメモアプリを作成しました。
このアプリに検索機能を追加します。環境
Swift5
Xcode13.1
iOS15
(iOS15以上で使えるsearchable
というモディファイアを利用します。)
検索絞り込み機能を実装する
メモアプリのソースコードから一覧表示部分を抜粋します。
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: Memo.entity(),
sortDescriptors: [NSSortDescriptor(key: "updatedAt", ascending: false)],
animation: .default
) var fetchedMemoList: FetchedResults<Memo>
var body: some View {
NavigationView {
List {
ForEach(fetchedMemoList) { memo in
NavigationLink(destination: EditMemoView(memo: memo)) {
VStack {
Text(memo.title ?? "")
.font(.title)
.frame(maxWidth: .infinity,alignment: .leading)
.lineLimit(1)
HStack {
Text (memo.stringUpdatedAt)
.font(.caption)
.lineLimit(1)
Text(memo.content ?? "")
.font(.caption)
.lineLimit(1)
Spacer()
}
}
}
}
.onDelete(perform: deleteMemo)
}
.navigationTitle("メモ")
.navigationBarTitleDisplayMode(.automatic)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: AddMemoView()) {
Text("新規作成")
}
}
}
}
}
private func deleteMemo(offsets: IndexSet) {
offsets.forEach { index in
viewContext.delete(fetchedMemoList[index])
}
try? viewContext.save()
}
}
それではこの一覧表示に検索機能を追加していきます。
- 検索ワードを監視するためのプロパティを追加する
@State private var searchText: String = ""
-
searchable
モディファイアの実装
NavigationView {
// 省略
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "検索")
placement
で表示位置を決められるのですが、今回は常に表示させるため.navigationBarDrawer(displayMode: .always)
を使ってます。
またprompt
でプレースホルダーの設定ができます。
-
onChange
で入力をする度に検索条件の判定を行う
// searchableの下に追加
.onChange(of: searchText) { newValue in
search(text: newValue)
}
-
onChange
で呼ばれるsearch
メソッドを実装
CoreDataはNSPredicate
を使うことで検索条件を指定することができます。
private func search(text: String) {
if text.isEmpty {
fetchedMemoList.nsPredicate = nil // ①
} else {
let titlePredicate: NSPredicate = NSPredicate(format: "title contains %@", text) // ②
let contentPredicate: NSPredicate = NSPredicate(format: "content contains %@", text) // ③
fetchedMemoList.nsPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [titlePredicate, contentPredicate]) // ④
}
}
ポイント
①検索が空の場合、検索条件の絞り込みはなし
②%@
は文字列として扱うという意味
「titleに文字列textが含まれている」を検索条件とする
③「contentに文字列textが含まれている」を検索条件とする
④NSCompoundPredicate
で複数条件の連結
orPredicateWithSubpredicates
は配列の中身をOR条件で連結する
これにより、タイトルかメモの中身のどちらかが検索ワードに部分一致するメモだけを抽出することが可能となります。
全体像
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: Memo.entity(),
sortDescriptors: [NSSortDescriptor(key: "updatedAt", ascending: false)],
animation: .default
) var fetchedMemoList: FetchedResults<Memo>
@State private var searchText: String = ""
var body: some View {
NavigationView {
List {
ForEach(fetchedMemoList) { memo in
NavigationLink(destination: EditMemoView(memo: memo)) {
VStack {
Text(memo.title ?? "")
.font(.title)
.frame(maxWidth: .infinity,alignment: .leading)
.lineLimit(1)
HStack {
Text (memo.stringUpdatedAt)
.font(.caption)
.lineLimit(1)
Text(memo.content ?? "")
.font(.caption)
.lineLimit(1)
Spacer()
}
}
}
}
.onDelete(perform: deleteMemo)
}
.navigationTitle("メモ")
.navigationBarTitleDisplayMode(.automatic)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: AddMemoView()) {
Text("新規作成")
}
}
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "検索")
.onChange(of: searchText) { newValue in
search(text: newValue)
}
}
private func deleteMemo(offsets: IndexSet) {
offsets.forEach { index in
viewContext.delete(fetchedMemoList[index])
}
try? viewContext.save()
}
private func search(text: String) {
if text.isEmpty {
fetchedMemoList.nsPredicate = nil
} else {
let titlePredicate: NSPredicate = NSPredicate(format: "title contains %@", text)
let contentPredicate: NSPredicate = NSPredicate(format: "content contains %@", text)
fetchedMemoList.nsPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [titlePredicate, contentPredicate])
}
}
}
参考
Discussion