ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftとSwiftUIへのCore Dataの並行処理の導入
Core DataがSwift 5.5の新しい並行処理機能をどのように採用し、より簡潔で効率的、かつ安全な非同期コードを実現しているかを紹介します。並行処理で動作するようにApp内のCore Dataを更新する方法を示し、SwiftとSwiftUIでの作業をより表現力豊かで強力なものにするフレームワーク全体での他の多くの改善点を詳しく説明します。
リソース
関連ビデオ
WWDC21
-
ダウンロード
♪ ♪ 皆さん こんにちは 私はCore Dataチームの マイケル・リーヒューです 本日は Core DataとSwiftの 機能が 目覚ましく 向上したことについて お話できることを 嬉しく思います まずCore DataがAppleの 全プラットフォームにおいて データの永続化という お客様の ニーズを満たすことを お話します そして Core Dataに 新たに採用した Swiftランタイムの 同時実行処理における 方法を 議論を交えて お届けします 次に Swiftで より表現豊かなコードを 作成するための Core DataAPIの 改善点について紹介します 最後に 2020年に 既存のSwiftUIサポートに 追加した Core Dataの 動的機能を紹介して 締めくくろうと思います では始めましょう どのようなAppleの プラットフォームの 開発にも まず Appが必要です そのAppは どこかの 時点で ユーザーデータの保管が 必要となります そこで最適な選択は Core Dataを使うことです Core Dataは特徴豊かな Appleのフレームワークで ユーザーデータを 強固かつ機能豊かな状態で 長期間保持したい 開発者に向けたものです このフレームワークは メモリ内での表現方法から ストレージ内での モデル化方法など 複雑な処理を多くの担い ユーザーデータを 適切に管理します このフレームワークはまた メモリの使用量や 待ち時間などの ランタイムの 重要事項を管理します このフレームワークの 機能は拡張も可能です 例えば簡単なローカル ストアから始めて 機能を拡張することにより 複数の実行コンテキストを 使用することもできます またCloudKitを介して 強力なデータを 共有することもできます Core DataはAppleの全ての プラットフォームで使えます この最後の点を 強調したいと思います Core Dataを使い始めると Mac iPhone AppleWatchなどの プラットフォームで 学んだことを 生かすことができます 勿論Core DataはSwiftを 使っても上手く機能します Core DataAPIが Swiftを用いても同程度に 表現力豊かになれるように 数年間開発を 続けてきました そして今年は Swiftにとって かなり刺激的な年です 言語とランタイムにおける 新たな同時実行処理機能を 導入しました Core Dataの開発当初から コードの同時実効性を 意識してきました これには十分な 理由があります データの永続化には 外部記憶媒体との間の 読み書きが必要となります 新規同時実行モデルが 上手く調和するように サポートします これが地震のサンプル Appの コンテキストで どのように 機能するかをお見せします このAppは 米国地質調査所の データフィードから 過去に起きた地震の マグニチュード 位置 日時などの データを読み取り Core Dataを使って 保存します 構造的にEarthquakesは Swift Appケーションです UIを起動するための ビューコンテキストと 地質調査所からデータを 取り込むバックグラウンド コンテキストがあります App用の ローカルコンテナに地質調査 所のJSONフィードから 地震データを収集します
ダウンロード時にデータを JSONパーサーに送信し バックグラウンドコンテ キストにインポートします そして管理 オブジェクトに変換し ローカルストアに保存します 次にビューコンテキストは 変更点を融合しUIを 魔法のように更新します 2020年にはこのデータを バッチ操作を用いて 効率的に処理する事に 焦点を当ててきました しかし今はこれらの操作を 同時に実行させることに 焦点を当てています 特にデータを Appに インポートする際の 3つのステップに 焦点を当てます ステップ1は 生データのダウンロードが 完了すると Appは それを特定のローカルな 表現に変換する 必要があります 最後にこの新たな オブジェクトを永続的な ストアに保存します ハイレベルコードに 変換してみましょう 各操作を独自の機能 又はクロージャに 要約しました Appはまずサーバから 生データを取得し ローカル表現に変換します 次に バックグラウンドで managedObjectContextに バッチ挿入を要求し オブジェクトをCore Dataに インポートします
このように書くと 潜在的な難所を 視覚化しやすく なります ネットワーク経由で データをロードすると 非同期処理を 検討するようになります 変換も考慮すべき点 でしょう さらに永続的なストアへの データのインポートは 極めて適切です
しかし今までは これら全ての ケースにおいて 非同期で処理するには 自身で実行するか又は 既存の枠組みで 処理するかに大きく 頼ってきました ここでCore Data特有の 抽象化についてお話します
PerformAndWait を呼び出すと 管理オブジェクトの コンテキストが 独自の保護環境下において 提供されたクロージャを 実行します これは作業が完了するまで 呼び出しスレッドと 関連する可能性があります
これを視覚化するとすれば 3つのブロックのコードを イメージできます BEFORE, DURING そして AFTERと ラベル付けをしました
コードが実行されると 最初にBEFOREの ラベルのコードが 元のスレッドで実行されます 次にperformAndWaitを 呼び出すと DURINGのクロージャの 処理が完了するまで スレッドはブロックされます その作業完了後AFTERの コードが実行されます
クロージャの終了を 待つ必要がない場合は 完全に非同期の バリアント型を 常時提供しています しかし今年Swiftが強力な 同時実行モデルを獲得し 深層言語も統合した ことにより APIの意図を正確に 説明できるCore Dataが 実現しました 構文は少し異なります 貴方は実行の結果を 待つように指示しますが この新たなAPIを使用する ためのメンタルモデルは 管理オブジェクトを 常にサポートしてきた ものと全く同じです 利点としては 同時処理の詳細が もはや隠されておらず 代わりにSwift言語に 深く統合されている ということです このためコンパイラは 一般的な 同時実行バグを多く 自動的に防止することが できます 例えばデータ競合や デッドロック さらには タスクが待ち状態の時の 効率的なリソースの 活用などです コードに戻って 実際に 使った場合どのような感じ なのかを見てみましょう
前述のように非同期関数に 待機を指示します 前の状態に戻り 非同期関数が 制御を回復するまで 呼び出し実行コンテキストが 中断する可能性があります
Swiftの既存の構造化エラー 処理機能とともに 途切れなく機能し 呼び出しフレームに 提示されたエラーを 経路制御します 非同期関数呼び出しの例を 見てきました コンテキスト内で 非同期作業を 実行するためにどのように 宣言されているのか Core Dataの 新規の方法を 見てみましょう このSwiftコードの一節に かなりの機能が 詰め込まれています まずは重要な点について いくつか紹介し その後 実際に どのように使うかを お見せしたいと思います 新たな実行オーバーロードの 宣言をすると 結果に基づき 戻すことができる非同期 キーワードで装飾された 一般的なものであることが わかります この機能が 新たなSwiftの 同時実効性の 機能を選択します この新たなAPIの優れた点は 提示されたクロージャにより エラーをスローしたり 値を戻したりすることが 出来ることであり 手動で呼び出しフレームに ルーティングするよりも 手間を省くことが 出来ることです これがどれほど凄いことかを いくつかのシナリオに沿って 見てみましょう これまでは同時実行機能が 実装内部に隠れていたため エラーを転送する 唯一の方法は performAndWait外の オプションで閉じた後に チェックする ことでした これは完全に非同期の バージョンを 使用すると 更に複雑になります なぜなら 完了ハンドラを通過する 多くのプラミングし それを 一貫して使用することを 確認する必要が あるからです 新たな同時実行モデルを 備えたSwiftでは 全てのプラミングが 処理されます 一度試して非同期での作業を 見てみて下さい エラーが発生した場合は スローするだけで自然に 呼び出しフレームで 解決されます
エラーに注目しましたが 結果はどうでしょう? 私が説明した事は全て 同じ様に機能します 具体例を 見てみましょう コードに入る前に 概略をお話しましょう この例では 過去5時間に発生した 地震の数を 確定するための フェッチリクエストを 設定します 文章であれば 表現するのは 簡単です しかしコードでは 少し命令し直します はじめに5時間前を 定義する必要があります これを確実な方法で 計算するには カレンダーAPIを 使用します 次にその日の フェッチリクエストを 述語付きで構成し カウント結果を 要求します コードは ほぼ計画通りです 今から5時間前を 計算するために カレンダーの オフセットAPIを使用します 次に述語付き カウント結果を 一致する日付に帰すために Quake FetchRequestを 構成します これまでは結果を返した場合 エラーを攻略した時と 似たパターンをたどりました 変更が必要な時 どのような 状態でも一旦閉じてから managedObjectContextで 計算を実行します コントロールを取り戻した後 結果を使用します
そして実行コールの結果を ただ待ちます その結果を 実行ディレクトリ用に 呼び出しフレームに 返します 残りのコードは 全く同じです 回避すべきなのは手作業での 値のルーティングと そのコードの 潜在的なバグと ニュアンスです この新たなコードは非常に 簡潔で表現力豊かです
ただし注意が必要な時も あることをお伝え しなければなりません 別の例でその理由を 見てみましょう この例では直近の地震を 管理オブジェクトとして 返そうとしています 新たなAPIではたやすく 値を返せますが既に managedObjectContextに 登録されている managedObjectsを返す ことは安全ではありません 登録されたオブジェクトは 実行呼び出しの クロージャ内で 参照のみできます managedObjectを 参照する必要がある場合は 異なる実行コンテキスト間で ObjectIDを利用して 必要に応じて リフェッチするか 辞書表現オプションの フェッチリクエストを 利用します 別の例を お見せする前に お伝えすることが あります それは ScheduledTaskTypeです これまで見てきた 全ての非同期処理は オプションに関しては デフォルト値の .immediateです 2番目のオプションは .enqueuedです この2つのアプローチの 違いを理解することにより スケジュール作業を 指示する際に ManagedObjectContext内で 何が起こるかについて 知ることができます 前述の通りimmediateは performAndWaitの Swift-async-aware版 のような動作をします 別の実行コンテキストを バックグラウンドコンテキス トで走らせているのであれば その作業を待つように指示します そしてスケジュールがなされ 作業が完了するまで待ちます
ただし既に同じ 実行コンテキストにいる場合 作業は楽観的に すぐに予定されます
一方enqueuedは もう少しシンプルです 発信元の呼び出しサイトとの 相性とは関係なく ただ常に要求された作業を コンテキストの ワークセットの最後まで 追加するだけです 先に進んで もう1つ 例を見てみましょう これら全ての非同期機能を 貴方も使うことが可能です ここに これまでお話してきた インポート論理を 新たな非同期キーワードで 装飾された importQuakes関数に 因数分解しました この関数は順に 他の非同期機能に対して 実行されます
この新たなSwiftの 同時実行機能を 活用するためのこの関数を 待てば良いだけです これまで見てきたことを まとめてみましょう この新たなAPIによりSwiftの 構造化された 同時実行性が 直接Core Dataに 活かされます 実行APIの新規 バリアント型は既存の Core DataAPIのSwift の同時実行性を意識した バージョンで既に 愛用されています 貴方のAppで この新たなAPIを 利用されることを 強くお勧めします
さらに 保護された 同時実行ドメイン内で タスクをサポートする Core Dataは NSManagedObjectContext だけではありません NSPersistentContainer と NSPersistentStore Coordinatorに類似 したAPIも追加しています これらのAPIの全体的な 形状と動作は既に説明した ものと似ています しかし その同時処理力を もってしても 既存の利用可能な デバッグツールの 使用を お勧めします XCodeはアドレスと スレッドサニタイザーを 提供し 認知度が非常に 低いバグでさえも 捕らえることができます 両方とも スキームエディターの 設定の診断ペインで 見つけることができます 各サニタイザーは 複数のスレッドから 安全なメモリ使用の検証や データの適切な使用など 様々な問題を 検出します ソフトウェアをユーザーの コミュニティにリリースする 前に常にAppと 関連するテストを サニタイザーを使って点検 することは好ましい事です サニタイザーが全ての コンテキストで役立つ一方で Core Dataにより ドメイン固有のヘルプを 利用できる特別な ランタイムフラグも 使えることをお伝えします このオプションを有効にする とCore Dataは内部ロックを 検証し様々なタイプの Core Dataを適切に使用する ための多くの有用な アサーションをオンにします
Swiftの同時実行性の サポートの採用だけが 今年Core Dataに加えられた 変更ではありません CloudKitの共有から 新たなSpotlightの統合まで これまで紹介してきた 新たなAPIは全て Swiftを念頭に置いた プレゼンテーション とともに 作成されました 今年はこれらの トピックごとに セッションを設けているので チェックされることを お勧めします さらにフレームワーク 全体を通して Swiftで改善できる場所を 特定するための パスを作りました これから私達が サポートする 様々な永続的 ストアをいくつか お見せします Recallは 永続的なストアで 顧客のデータを保存する 物理的な方法を説明します Core Dataは現在4つの ストアを提供しています XML Binary InMemoryそしてSQLiteです 貴方はこれらの識別子を 常に使用しています 今年はこれらに Swiftにおける 自然な名前を付けました 既存の名前はこれからも 使用可能です しかしこれらを 消費する新たなAPIは 簡潔な名前と記号の オートコンプリート 機能により 更に人間工学的に 使用されるようになります もちろん永続的ストアだけが Core Dataの型に 関与している訳では ありません 型付きデータの保存に 関してはフレームワークが 全てでありそのような型が 属性とともに記述されます そして今年は新たな 拡張可能な列挙型を 追加することにより 各型で遙かに自然な 構文を用いて属性を 説明できるようになりました XCodeモデルビルダーで ランタイムモデルが 設計したものと 一致するかどうかを検証する ユニットテストを書いて 動作を確認してみましょう
簡潔にするために 地震オブジェクトモデルで 定義された単一のランタイム について検証します これにより測定方法を イメージすることができます これは小テストのように 思えるかもしれませんが 将来は よりスピードアップし 深く探究できるので 検証することは 好ましいことです 新規のAttributeType の点について 簡単なヘルパー関数 を書き テストとします この関数について 説明しましょう まずは属性名 実体の説明さらに 新規のAttributeType 列挙型の説明 を含む 署名をします
このユーティリティの定義は 非常にシンプルです 最初に指定された名称の 属性があることを 確認します これがないと テストに失敗します そして属性の型が 想定通りかどうかを 検証します 本当に それだけのことです これを実体とプロパティ ごとに繰り返します そしてランタイムの動作が 定義したモデルと一致する ことを確認します
これは今年 Swiftにおいて Core Dataの列挙子が 人間工学的に 改善されたサンプルです
ここまで下位レベルでの フレームワークの相互作用が Swiftでどのように表れるか についてお話してきましたが ユーザーへのプレゼンテー ションはどうでしょうか? 2020年はCore Dataを SwiftUIで操作することの 利便性について 多く紹介してきました ここで私の同僚である スコットが 今年我々が成し遂げた 向上した機能について ご紹介します スコット ありがとうマイケル! 今年Core Dataと SwiftUIを用いた 機能において 改善された点は 多くあります まずはフェッチリクエスト におけるレイジー エンティティの改善で イメージを構築する前に Appにセットアップ されているCore Dataの スタックの要件の緩和です 今年もフェッチリクエストは ソート記述子と述語の 動的構成を ピックアップします
そしてセクションフェッチ をサポートする新たな 種類のフェッチリクエスト があります 先ほどマイケルが紹介した 地震サンプルApp を用いて レイジーエンティティの 解決をはじめとして 個々に お話していきます 貴方のAppにはこのような コードがあると思います このコンテナプロパティは このタイプのコードや さらに広域のAppを サポートするのに実際 のところ必要ありません QuakesProvider型から 必要なものを 直接入手することが できます このプロパティは このモデルを ロードする前に エンティティを 参照する段階で Core Dataスタックが 設定されていることを 確認するためにあります こちらをご覧ください ContentViewの 初期化の後に 環境ビュー修飾子が 呼び出されています 今年のSDKに 配置する時は このトリックは もはや必要ありません FetchRequestの プロパティラッパーが フェッチ時にレイジーの名で エンティティを検索します この時点でCore Data スタックの設定環境が 保証されています ですのでこのプロパティを 今削除しても安全です
そして直接
環境呼び出しで QuakesProviderが 共有するコンテナを 参照します 新規APIに移ります FetchRequestが動的構成を サポートします リクエストの述語を直接変更 するためのラップされた 値には新規プロパティが 2つあります またソート記述子は 使い慣れた NSSortDescriptorsと 自動生成された 管理対象オブジェクトの サブクラスの エンティティを フェッチするときに 利便性と安全性が高い新規 SortDescriptor value タイプのものがあります
そして最後に ラップされた値と同じ プロパティセットを備えた 構成バインディングがあり ビューとの統合を 容易にします この新規APIの登場前は ソートと 述語パラメーターを ビューのイニシャライザ を通して イメージをデザインする 必要がありました また ツールバーのコントロールを 使用してフェッチリクエスト の設定をしたり難しい 作業が必要でもありました この摩擦は新たな 動的構成プロパティにより 排除されます これから 地震サンプルAppへの 追加 並べ替え フィルタリングなどの 使用方法をお見せします まずソート記述子を 見てみましょう デフォルトでは地震Appは 時系列でソートされています 更にマグニチュードで ソート されたデータも 欲しい場合は メニューを 追加して結果の順序を 制御できるようにします まずサポートしたい ソート記述子を含む
タプルの静的配列を 名称とともに 追加します すると新しいSortDescriptor タイプも使用されています
どの並べ替え順を 使用してきたかを 追跡する機能も 欲しいですね このための型を既に 作成してあるので コンテンツビューの プロパティとして追加します 次にツールバーメニューを リストビューへ追加します
onChange修飾子とともに 選択後のものに変更され フェッチリクエストの ソート記述子を更新します
プレビューでは新しい メニューを見ることができ 地震データをマグニチュード でソートすることができます 素晴らしい! フィルタリングを追加します 地震の場所でフィルタリング したいと思います 最初に必要なのは 検索フィールドの テキストのステートです そしてフェッチリクエストを 更新する検索フィールド用の
バインディングプロパティを 作ります
これらを配置すると あとはUIだけです 便利なことに
Searchableは文字列と バインドするため ここに落とすだけで いいのです
プレビューでは この新たなフィールドに ”サンドイッチ”と入力 するだけでマッチした 震源地の近隣地域を 絞り込むことができます
これがフェッチリクエストの 動的構成です 他に機能面でよく要求される ことはセクションフェッチの サポートです これは SectionedFetchRequestと 呼ばれる新たな プロパティラッパーとして 今年登場します このタイプはFetchRequest として同じ新たな動的構成 プロパティをサポートします しかしこれは NSFetchedResultsController に似てセクションを識別する プロパティへのキーパスの 追加パラメータとともに 初期化されます
しかしフェッチ結果 コントローラーとは違い セクションを識別する プロパティは ハッシュ可能であれば 任意のタイプを選べます SectionedFetchRequestの 追加のジェネリック パラメータを使用して 型システムに エンコードされます 最後にこの新たな型を 2次元の結果型にまとめます SectionedFetchResultsは セクションの集まりで そのそれぞれが 結果の集まりです 各セクションには識別子 付きのプロパティもあります
これは非常に簡単に取り入れ られるので地震Appに セクションフェッチを 追加してみたいと思います まずフェッチリクエスト 宣言を更新します
Quakeには既に日付の プロパティがあるので それをセクショニング キーパスに使います
次にボディプロパティを 更新して
新たなセクション化 結果型と一致させます
ここで外側のループが セクションを繰り返すので セクションビューを 出力します そして各セクションは 地震の集まりで 先程結果を 繰り返していたように この内側のForEachが セクションを繰り返します
プレビューを見ると 時系列で日ごとに 区分された 地震結果を得ました そしてSwiftUIは自動的に セクションを折りたたむ サポートを備えています
この新たな SectionedFetchRequest型は セクション識別子 キーパス用の追加構成 プロパティに加えて FetchRequestと同じ 動的構成プロパティも サポートしています 並べ替えを変更するのは もはや安全ではないので これは 非常に重要です 時間と地震の マグニチュードは 完全には相関していないため セクションが断続的に なる可能性が あるからです これを修正するには並べ替え を更新する必要があります
それぞれにセクション識別子 キーパスが付与されました
ツールバーを下に行きます
ソート記述子を 更新する度に セクション識別子キーパスも 更新する必要があります
ここが重要な部分です リクエストの変更は 結果ゲッターが 呼び出された場合は常に コミットされます 並べ替えとセクショニングの 両方を安全に更新するには
参照の構成を 引き出した ローカルの結果に 更新する必要があります
プレビューでは順序を 変更するとセクショニングも 変更されていることが 確認できます 地震を時系列に並べて 日付けで区分する事と マグニチュード順に 並べて区分する事を 切り替えることができます
これで完成です スタックのレイジー初期化 動的構成及びセクション フェッチはiOS15と MacOS Montereyを用いれば 全て既存のAppに簡単に 適用できます つまりCore DataはAppleの 全プラットフォームにおいて Appデータの永続性 ニーズを満たすワンストップ ショップのようなものです 新たな同時実行性機能を 備え 実行APIを介して Swiftで利用可能であり 強力なスレッド安全性 デバッグ機能も 組み込まれています
新たな列挙型インター フェイスはSwiftでのストア と属性タイプの使用を サポートし更にはCloudKit の共有及びSpotlightの 統合を実現しました SwiftUIを利用するとビュー へのデータ接続が未だ かつて無い程に簡単になり 動的構成とセクション フェッチ機能も備えています
このトピックに関連する 新たに学ぶべきことが まだ多くあります 是非"SwiftUIで簡素化し Swiftで同時実行性を 満たそう”をチェック されることをお勧めします これで終わりです! 皆さんが新規APIを 使って作り上げたものを 拝見することを とても楽しみにしています [明るい音楽]
-
-
20:36 - FetchRequest dynamic configuration: sort descriptors
private let sorts = [( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .reverse)] ), ( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .forward)] ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)] ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .forward)] )] struct ContentView: View { @FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: FetchedResults<Quake> @State private var selectedSort = SelectedSort() var body: some View { List(quakes) { quake in QuakeRow(quake: quake) } .toolbar { ToolbarItem(placement: .primaryAction) { SortMenu(selection: $selectedSort) .onChange(of: selectedSort) { _ in let sortBy = sorts[selectedSort.index] quakes.sortDescriptors = sortBy.descriptors } } } } struct SelectedSort: Equatable { var by = 0 var order = 0 var index: Int { by + order } } struct SortMenu: View { @Binding private var selectedSort: SelectedSort init(selection: Binding<SelectedSort>) { _selectedSort = selection } var body: some View { Menu { Picker("Sort By", selection: $selectedSort.by) { ForEach(Array(stride(from: 0, to: sorts.count, by: 2)), id: \.self) { index in Text(sorts[index].name).tag(index) } } Picker("Sort Order", selection: $selectedSort.order) { let sortBy = sorts[selectedSort.by + selectedSort.order] let sortOrders = sortOrders(for: sortBy.name) ForEach(0..<sortOrders.count, id: \.self) { index in Text(sortOrders[index]).tag(index) } } } label: { Label("More", systemImage: "ellipsis.circle") } .pickerStyle(InlinePickerStyle()) } private func sortOrders(for name: String) -> [String] { switch name { case "Magnitude": return ["Highest to Lowest", "Lowest to Highest"] case "Time": return ["Newest on Top", "Oldest on Top"] default: return [] } } } }
-
21:33 - FetchRequest dynamic configuration: predicates
struct ContentView: View { @FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: FetchedResults<Quake> @State private var searchText = "" var query: Binding<String> { Binding { searchText } set: { newValue in searchText = newValue quakes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "place CONTAINS %@", newValue) } } var body: some View { List(quakes) { quake in QuakeRow(quake: quake) } .searchable(text: query) } }
-
23:26 - SectionedFetchRequest
extension Quake { lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "MMMM d, yyyy" return formatter }() @objc var day: String { return dateFormatter.string(from: time) } } struct ContentView: View { @SectionedFetchRequest( sectionIdentifier: \.day, sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: SectionedFetchResults<String, Quake> var body: some View { List { ForEach(quakes) { section in Section(header: Text(section.id)) { ForEach(section) { quake in QuakeRow(quake: quake) } } } } } }
-
24:56 - SectionedFetchRequest dynamic configuration: sort descriptors
extension Quake { lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "MMMM d, yyyy" return formatter }() @objc var day: String { return dateFormatter.string(from: time) } @objc var magnitude_str: String { return "\(magnitude)" } } private let sorts = [( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .reverse)], section: \Quake.day ), ( name: "Time", descriptors: [SortDescriptor(\Quake.time, order: .forward)], section: \Quake.day ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)], section: \Quake.magnitude_str ), ( name: "Magnitude", descriptors: [SortDescriptor(\Quake.magnitude, order: .forward)], section: \Quake.magnitude_str )] struct ContentView: View { @SectionedFetchRequest( sectionIdentifier: \.day, sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)]) private var quakes: SectionedFetchResults<String, Quake> @State private var selectedSort = SelectedSort() var body: some View { List { ForEach(quakes) { section in Section(header: Text(section.id)) { ForEach(section) { quake in QuakeRow(quake: quake) } } } } .toolbar { ToolbarItem(placement: .primaryAction) { SortMenu(selection: $selectedSort) .onChange(of: selectedSort) { _ in let sortBy = sorts[selectedSort.index] let config = quakes config.sectionIdentifier = sortBy.section config.sortDescriptors = sortBy.descriptors } } } } struct SelectedSort: Equatable { var by = 0 var order = 0 var index: Int { by + order } } struct SortMenu: View { @Binding private var selectedSort: SelectedSort init(selection: Binding<SelectedSort>) { _selectedSort = selection } var body: some View { Menu { Picker("Sort By", selection: $selectedSort.by) { ForEach(Array(stride(from: 0, to: sorts.count, by: 2)), id: \.self) { index in Text(sorts[index].name).tag(index) } } Picker("Sort Order", selection: $selectedSort.order) { let sortBy = sorts[selectedSort.by + selectedSort.order] let sortOrders = sortOrders(for: sortBy.name) ForEach(0..<sortOrders.count, id: \.self) { index in Text(sortOrders[index]).tag(index) } } } label: { Label("More", systemImage: "ellipsis.circle") } .pickerStyle(InlinePickerStyle()) } private func sortOrders(for name: String) -> [String] { switch name { case "Magnitude": return ["Highest to Lowest", "Lowest to Highest"] case "Time": return ["Newest on Top", "Oldest on Top"] default: return [] } } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。