見出し画像

SwiftDataの検索・絞り込みと並べ替え

割引あり

多数の情報を表示する画面に検索や並べ替えは重要な機能です。
SwiftUI と SwiftData の組み合わせでリアルタイムの『検索・絞り込み』はひと工夫が必要です。

このnoteでは SwiftData の検索絞り込みと並べ替えを、シンプルな検索サンプルと絞り込みや並べ替えを含むサンプルで解説します。

執筆時点の環境:
Xcode 15.0.1
macOS 14.1.1

この記事は『SwiftData を iOS アプリでためす』マガジンで読むことができます。

『SwiftData を iOS アプリでためす』マガジンで読める記事:
SwiftDataをシンプルにためす
CSVファイルデータを読みSwiftDataで使う
アプリ起動時の表示情報にSwiftDataを使う
マクロ と オブザベーション
SwiftDataの検索・絞り込みと並べ替え
(この記事)
iOS18に追加されたSwiftDataの新機能 【2024年10月18日追加】


  • 画像クリックで拡大表示できます

  • 画像を拡大表示中は画像の左右をクリックで画像だけを順に表示できます

  • ソースコード部分は左右にスクロールできます

  • リンクしているドキュメントは英文が多いですが、翻訳機能を活用してください




🟠 動的に絞り込み条件を変更する

SwiftData のドキュメントは2023年11月10日現在も目立った解説文の追加はありません。

『検索』については SwiftDataLocalDataCacheSample サンプルの解説ページ「Filtering and sorting persistent data」に解説があります(サンプルのプロジェクト名とビルドしたアプリ名は DataCache です)。
このサンプルは世界の地震データにアクセスして日付ごとに絞り込み地震の規模や発生時刻で並べ替えて表示します。
地図上でグラフィックに表示もでき、発生場所の地名などでも絞り込みできます。
リッチな機能ですが SwiftUI と SwiftData のパワーでコードは比較的シンプルです。
検索フィールドは searchable(text:placement:prompt:) を使い、並べ替えは Menu と Picker を使っています。


🟠 検索フィールド

SwiftUI で検索フィールドは SwiftUI のビューモディファイア searchable(text:placement:prompt:) などを使います。
ドキュメントは Search(英文)があります。

検索についてはヒューマンインターフェースガイドラインの「検索」や「検索フィールド」も参照してください。

公式解説ドキュメント「Adding a search interface to your app」(英文)もあります。

この説明によると  searchable(text:placement:prompt:) などのビューモディファイアはツールバーに検索フィールドUIを追加するとなっています。

もう一つのドキュメント「Performing a search operation」は SwiftData より前の ObservableObject を使った例の解説です。
検索関連では他にも英文の Article があります。


searchable(text:placement:prompt:) モディファイア

DataCache サンプルでは searchable(text:placement:prompt:) の text 引数だけを使い  placement とprompt 引数は省略しデフォルトを使います。

searchableモディファイアの引数:

func searchable(
    text: Binding<String>,
    placement: SearchFieldPlacement = .automatic,
    prompt: Text? = nil
) -> some View

text 引数に検索文字列のバインディングを渡します。

placement 引数はビュー階層内の検索フィールドの配置を SearchFieldPlacement 型インスタンスで設定します。
automatic、navigationBarDrawer、sidebar、toolbar のほか navigationBarDrawer(displayMode:) が使えます。
prompt 引数は検索フィールドのプロンプトを表すTextビューです。



🟠 ContentUnavailableView

ContentUnavailableView は iOS 17 から利用可能になった、SwiftUI でコンテンツが利用できない時に表示するためのビューです。
DataCache サンプルではデータが何もない場合と、検索で一致するデータがない場合に ContentUnavailableView を使っています。

ContentUnavailableView の利用例

ContentUnavailableView のドキュメント:

検索で一致するデータがない場合を表示しているのは ContentUnavailableView.search です。

ContentUnavailableView は有用な新しい SwiftUI ビューですが、WWDC2023の「What’s new in SwiftUI(SwiftUIの新機能)」セッションでは解説されていません。(Codeでは1箇所説明なく使われている)


ContentUnavailableView.search

ContentUnavailableView.search は『検索結果なし』を伝える標準的なビューを作成します。

ContentUnavailableView.search とすると自動的にsearchableモディファイアが表示する検索フィールドの文字列を含む情報をユーザーに表示できます
(検索文字列のプロパティを明示する必要もありません)
アプリは表示文字列は何も用意する必要はありません

static var search: ContentUnavailableView<SearchUnavailableContent.Label, SearchUnavailableContent.Description, SearchUnavailableContent.Actions> { get }

ContentUnavailableView.search は ContentUnavailableView のタイププロパティです。


ContentUnavailableView のイニシャライザ

ContentUnavailableView は4つのイニシャライザがあります。
DataCache サンプルではそのひとつを使って何もダウンロードしていない状態をユーザーに表示します。

何もデータがない場合の表示

この表示は次のコードです。

.overlay {
    if viewModel.totalQuakes == 0 {
        ContentUnavailableView("Refresh to load earthquakes", systemImage: "globe")
    } else if quakes.isEmpty {
        ContentUnavailableView.search
    }
}

地震データ件数がゼロの場合は ContentUnavailableView("Refresh to load earthquakes", systemImage: "globe") をリストにオーバーレイして、quakes.isEmpty の場合は ContentUnavailableView.search をリストにオーバーレイしています。

条件により表示する/しないを切り替える場合は .overlay を使うのが SwiftUI の定石の一つです。

ContentUnavailableView のイニシャライザは init(_:systemImage:description:) を使っています。

init(_:systemImage:description:) はSFシンボルをその名称文字列で指定します。
そのほか画像をリソース名を指定するイニシャライザ init(_:image:description:) もあります。
第1引数 title の文字列はアプリで指定が必要で、ローカライズ文字データもアプリで対応します


ContentUnavailableView.search は多言語対応済みです。
日本語環境日本語対応アプリで実行すると次のように表示します。
さらに(SwiftUI なので当然ですが)ダークモードとDynamic Typeにも対応済みです。

ContentUnavailableView.searchは多言語対応済み

シンプルなコードで SwiftUI 標準の方法でアプリの状態をユーザーに表示できるので、アプリを iOS 17 以降対応にできる場合は ContentUnavailableView はおすすめです。


🟠 Predicate

検索文字列は Predicate を使って絞り込みます。

Predicate 型は iOS 17 から利用可能な Swift言語専用の struct です。
SwiftData ではなく Foundation フレームワークです。
絞り込みの条件には文字列だけでなく数値や日時データなどを利用可能です。


#Predicate マクロ

マクロ #Predicate があり、このマクロを使うと通常のSwift言語の条件式で条件を記述できます。
なじみのある通常の条件式で記述できるのでプログラマーには見やすく、記述のエラーも的確に指摘されます。

Xcode 15.0.1 の Jump to Definition でこのマクロを見ると

@freestanding(expression) public macro Predicate<each Input>(_ body: (repeat each Input) -> Bool) -> Predicate<repeat each Input> = #externalMacro(module: "FoundationMacros", type: "PredicateMacro")

と表示されます。(残念ながら#Predicateマクロのドキュメントはまだ見つけることができていません)

DataCache サンプルでは

return #Predicate<Quake> { quake in
    (searchText.isEmpty || quake.location.name.contains(searchText))
    &&
    (quake.time > start && quake.time < end)
}

の部分が38行に展開されます。
(searchTextが空の場合は日時データがstart からendのデータ、searchTextありならそれをlocatio.nameに含みかつ日時データがstartからendのデータに絞り込む)

#Predicateを展開した画面(一部)

展開された PredicateExpressions はドキュメントに載っていますが「Don't use this type directly. When you call the Predicate(_:) macro in your code, the expansion of that macro produces these values.」(このタイプを直接使用しないでください。コードでPredicate(_:)マクロを呼び出すと、そのマクロの展開によりこれらの値が生成されます。)と明記されています。


PredicateはQueryで使う

SwiftData で Predicate は Query で使います。
たとえば Query(filter:sort:animation:) マクロでは

@attached(accessor) @attached(peer, names: prefixed(`_`))
macro Query<Element>(
    filter: Predicate<Element>? = nil,
    sort descriptors: [SortDescriptor<Element>] = [],
    animation: Animation
) where Element : PersistentModel

最初の引数 filter が Predicate型(のオプショナル)です。
このマクロで絞り込みが可能ですが、絞り込み条件をダイナミックに変更することはできません。
@Queryマクロはビューのプロパティの属性として作用するので、ビューを更新することが必要になるためです。



🟠 検索機能の実装

検索フィールドに入力した文字列でただちに絞り込みその結果を表示する DataCache サンプルの検索機能については「Filtering and sorting persistent data」解説ページの「Define a filter using a predicate」と「Update a query dynamically」にコードの説明があります。


検索文字列と日付で絞り込み

Define a filter using a predicate」を参照してください。

モデルである Quake クラスに地震の場所名テキストと発生日時をチェックするフィルターを static func で追加しています。

// searchTextとsearchDateで絞り込むPredicateを返す関数
static func predicate(
    searchText: String,
    searchDate: Date
) -> Predicate<Quake> {  // 以下省略

検索文字列や指定日付が変わるたびにこのメソッドを呼び、新たな絞り込み条件の Predicate インスタンスを使います。


操作に対応して絞り込む

SwiftUIは宣言型の記述です、このため操作の対応処理は従来の手続き型のように記述することはできません。

絞り込む操作があるビューはイニシャライザを利用することでユーザーの「操作」に対応します。

SwiftUI の通常のビューは通常デフォルトのイニシャライザを使います(ビューは構造体なのでメンバワイズイニシャライザが自動生成される)、このためビューにイニシャライザを記述することはまれです。
メンバワイズイニシャライザはプロパティに値を設定するだけのシンプルなイニシャライザです。

DataCache サンプルでは絞り込みの条件を引数にしてビューのイニシャライザを追加することで、条件が変更になったら表示する地震データ配列をpredicate関数で更新した後にビューを再表示するしくみです。

struct QuakeList: View {
    @Environment(ViewModel.self) private var viewModel
    @Environment(\.modelContext) private var modelContext
    @Query private var quakes: [Quake]

    @Binding var selectedId: Quake.ID?
    @Binding var selectedIdMap: Quake.ID?

    init(
        selectedId: Binding<Quake.ID?>,
        selectedIdMap: Binding<Quake.ID?>,
        
        searchText: String = "",
        searchDate: Date = .now,
        sortParameter: SortParameter = .time,
        sortOrder: SortOrder = .reverse
    ) {
        _selectedId = selectedId
        _selectedIdMap = selectedIdMap
        // 続く

QuakeList ビューのイニシャライザは引数が6個あります。
QuakeList は選択した地震の震源地を地図で表示する/地図でタップした地震を表示する機能やソートも2種あるため引数の数が多くなっています。

実際に絞り込みを行うコードは

// 絞り込みを行う部分
let predicate = Quake.predicate(searchText: searchText, searchDate: searchDate)
switch sortParameter {
    case .time: _quakes = Query(filter: predicate, sort: \.time, order: sortOrder)
    case .magnitude: _quakes = Query(filter: predicate, sort: \.magnitude, order: sortOrder)
}

let predicate 部分は Quake クラスのタイプメソッド predicate を使い、検索文字列と日付で絞り込む Predicate インスタンスを代入しています。

Quake.predicate(searchText:searchDate:)部分の説明

Quake.predicate(searchText:searchDate:) はMap表示でも同じ条件で絞り込むために使っています。


絞り込みを表示に反映させるしくみ

ここから先は

11,919字 / 15画像 / 2ファイル
この記事のみ ¥ 300〜

今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。