ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftDataを使用したカスタムデータストアの作成
SwiftDataが提供する表現力に優れた宣言型のモデリングAPIの力を、デベロッパ各自の永続性バックエンドと組み合わせましょう。カスタムデータストアの構築方法と、アプリに永続性機能を段階的に追加していく方法を説明します。このセッションの内容を最大限に活用するには、WWDC23の「Meet SwiftData(SwiftDataについて)」と「Model your schema with SwiftData(SwiftDataでスキーマをモデル化)」を併せて視聴されることをお勧めします。
関連する章
- 0:00 - Introduction
- 1:21 - Overview
- 4:50 - Meet DataStore
- 7:42 - Example store
リソース
関連ビデオ
WWDC24
-
ダウンロード
皆さん こんにちは Luvenaです ここではSwiftDataにおける カスタムデータストアについて説明します これはデベロッパの永続性バックエンドで SwiftDataを使用する手法の1つです カスタムデータストアは SwiftDataの新機能であり これにより 任意のドキュメントやファイル形式 永続性バックエンドを 必要に応じて使用できます また 既存のSwiftDataコードとの 互換性もあります
こちらはSampleTripsアプリの 実装ですが ここでストアの型を変更するには ModelConfigurationを JSONStoreConfigurationに 置き換えるだけです このビデオの後半で 実際の実装をお見せします この1か所を置き換えるだけで 別のストアの型を使用することが ModelContainerで認識されます SampleTripsアプリの モデルやビューのコードに 変更を加える必要はありません
このビデオではまず SwiftDataにおける ストアの役割を紹介し ストアとModelContextや ModelContainerとの やり取りについて説明します また 新しいDataStore プロトコルを使用して ストアを構築する方法を説明します 最後に カスタムデータストアの 実装に関する基本事項を確認し 永続性を実現するために JSONファイルを使用する例を紹介します 大まかに言うと ストアは永続性モデルを支えるために 必要なすべてのデータの 取得と保存を担うものです SwiftDataにおけるカスタム データストアの機能について説明するため SampleTripsというアプリを使って どのように永続性が 実現されるかを確認します SampleTripsはSwiftUIとSwiftDataの 相乗効果を活かして構築されています 標準的なアプリは主に3つの部分で 構成されます SwiftUIは ユーザーインターフェイスを提供します 通常はリストやラベルなどのビューです そこにModelContextのモデルからの データが表示されます ModelContextは ModelContainerにある ストアを使ってデータを読み書きします このビデオでは SwiftDataにおいて ストアが果たす役割に 焦点を当てて説明します SampleTripsでは ModelContextを使用して ビューを実現し 旅行情報を表示します ModelContextでは ビューの旅行1件ごとに 永続性モデルのインスタンスを作成します また これらの旅行のそれぞれに 対応する永続識別子があり モデルを一意に識別できます ModelContextでは ユーザーによる変更がトラッキングされ 必要に応じてストアに 保存することができます たとえば ロサンゼルスへの 旅行をキャンセルして 東京への旅行を追加した場合 これらの変更はModelContextによって トラッキングされます 新しい東京のモデルが モデルのコンテキストに挿入される際 一時的なPersistentIdentifierで 識別されます ModelContextは保存する際 ストアに対して ロサンゼルスの 旅行を削除し 新しい東京への旅行を 挿入するよう 通知します ストアは東京のモデルに対して 永続的な永続識別子「Trip-5」を割り当て それまでの一時的な識別子 「Trip-t1」にマッピングします このプロセスを 「リマッピング」と呼びます
その後 ストアは東京旅行に対する 更新済みの永続識別子を 応答としてModelContextに 返します
ModelContextが状態の 更新を終えると UIでビューを更新して 旅行のレンダリングができます 変更の永続化は ModelContextと ストアの連携によって SwiftDataの永続性モデルが サポートされることを示す一例です 両者は取得や保存などの 操作が定義された 一連のリクエストとレスポンスを使用して やり取りします ストアの役割はモデルの値を 永続させるための 実装を提供することです このやり取りでは モデルを 送信可能でコード表現可能な形にした DataStoreSnapshotを利用します
SampleTripsアプリでは ビューは永続性モデルを使用して ModelContextとやり取りします ただし ModelContextが ストアとやり取りする必要がある場合は スナップショットを作成して モデルの現在の状態を保持します スナップショットは その時点で モデルに存在する値を格納する 送信可能でコード表現可能なコンテナです 永続性モデルと同様 それぞれが永続識別子で 識別されます
ストアはそれらのスナップショットを使用し その値をストレージに適用します その逆も同様です ModelContextによって ストアからデータが読み取られると ストアは一連のスナップショットを 作成します これは コンテキストで求められている永続性モデルと一致します
ModelContextはスナップショットごとに 永続性モデルを作成します このモデルをビューやクエリ そしてコンテキストが実行する その他の処理に使用します ストアはSwiftDataで 重要な役割を果たし それによりModelContextは どのような保存形式でも モデルのデータの読み取み/書き込みができます 新しいDataStoreプロトコルとその実現方法をご説明します
ストアには3つの重要な要素があります ストアについて記述した設定 モデルの値をModelContextと やり取りするためのスナップショット ModelContainerが管理できる ストアの実装 です これらの各要素はそれぞれ異なる 3つのプロトコルに準拠しています DataStoreConfigurationと DataStoreSnapshot DataStoreです SwiftDataのデフォルトのストアでは 次の3つの型が独自に実装されています ModelConfiguration DefaultSnapshot DefaultStoreです DefaultStoreは移行や 履歴のトラッキング CloudKitの同期など SwiftDataに備わる充実した機能を すべてサポートしています パフォーマンスとスケーラビリティに関する プラットフォームの ベストプラクティスが組み込まれており 永続性モデルに 最適なデフォルトの選択肢となっています
DataStoreプロトコルでは 保存 取得 キャッシュなど ModelContextが ストアを使用するために SwiftDataに必要な機能が すべて定義されています そのほかのプロトコルでは オプションの データストア機能が定義されており ストアに対して行われた変更を すべて記述するための 新しいHistoryプロトコルなどがあります
ModelContextは DataStoreプロトコルからの リクエストとレスポンスを使用して ストアとやり取りします たとえば ストアからデータを取得する場合 ModelContextはストアに対して ストアが取得すべきデータが記述された FetchDescriptorを含む DataStoreFetchRequestを 送信します
ストアは モデルの値を取得すると モデルごとにスナップショットを作成し DataStoreFetchResultに 格納して返送します
ModelContextは スナップショットごとに 永続性モデルを作成します
ModelContextのモデルが変更され saveが呼び出された場合も 同様の処理が行われます ModelContextは 変更された すべてのモデルのスナップショットを含む DataStoreSaveChangesRequestを 作成し そのリクエストをストアに送信します
ストアはスナップショットを ストレージに適用し DataStoreSaveChangesResultを 作成して ModelContextに返送します 最終的に ストアは「Trip-t1」など 新しく挿入されたモデルに対応する リマッピングされた識別子のマップを 提供します これをもとに ModelContextは 挿入された旅行の永続識別子を 「Trip-5」に更新します
最後に ModelContextは ストアからの保存結果を処理し 状態を更新して 挿入された旅行に対して 新しい永続的な永続識別子を割り当てます
ここまでDataStoreの仕組みを 説明してきました ここで 実際の実装の様子を 見てみましょう SampleTripsアプリで JSONファイルを使用して モデルを永続化する ストアを実装します 本題に入る前に 説明しておきたいことが2つあります このストアは「アーカイブストア」です つまり 読み込みまたは書き込みの際 ファイル全体が読み込まれます さらに Foundationで用意されている JSONコーダを使用し データをスナップショットの配列として ファイルに保存します
ストアを作成する最初の手順は 設定とストアの型を 宣言することです これはDataStoreConfiguration および DataStoreプロトコルに 準拠するものです
これらの型では 関連付けられた型を 使用して相互に参照します Configurationでは Storeの型として JSONStoreを設定し このストアでは Configurationに JSONStoreConfigurationを設定します
また JSONStoreでは ModelContextとのやり取りに 使用するスナップショットの型を宣言します ここで DefaultSnapshotを使用するのは モデルデータのエンコーディングや デコーディングをカスタマイズする 必要がないためです これで DataStoreをModelContextで 使用するために必要な 2つのメソッド fetchとsaveの 実装を始めることができます ModelContextが DataStoreFetchRequestを送信したら ストア内にあるデータを読み込み DataStoreFetchResultを インスタンス化する必要があります DefaultSnapshotは コード化できるため JSONDecoderを使用して Configurationで提供されている ファイルのURLから ストアのデータを読み込むことができます 次に DataStoreFetchResultを インスタンス化し ファイルから取得した スナップショットを設定して返します 現時点では この実装では FetchDescriptorにある述語や ソートコンパレータは 処理されません 述語やソートコンパレータの変換は 複雑なプロセスになる可能性があります ここではその代わりに ModelContextを使用します
リクエストに述語または ソートコンパレータが 含まれている場合は preferInMemoryFilterおよび preferInMemorySortの エラーをスローします この場合はこれで問題ありません メモリに読み込むことができる 小規模なデータセットだからです これでクエリとソートに対応できる 十分な機能を持つ fetchを実装できました fetchが実装できたら スナップショットをJSONファイルに 書き出す saveを実装できます saveの実装では 挿入 更新 削除の 3種類の変更を考慮し これらに対応したいと思います saveのリクエストで受信した スナップショットの処理を開始する前に まずファイルの現在の内容を 読み取る必要があります これについては 私が定義したreadという 別のメソッドで処理します すべてのスナップショットを辞書にまとめ 永続識別子をキーとします この辞書を作業用のコピーとして 新しいJSONファイルを作成し 最終的にディスクに書き込みます
saveのリクエスト内の 挿入されたモデルの スナップショットを処理します この処理には 挿入された 各スナップショットの識別子の割り当てと リマッピングが含まれます これについて もう少し詳しく説明します 先に説明したように モデルがストアに挿入されるときには 各モデルには一時的な識別子が含まれており そこにはストアが関連付けられていません この挿入された各スナップショットについて 新しい永続的な永続識別子を作成します 次に 新しい永続識別子を使って スナップショットのコピーを作成します この新しい永続識別子は remappedIdentifiers辞書内の 一時的な識別子にマッピングされ 後でsaveの結果で ModelContextに返されます 最後に 挿入されたスナップショットを 最初にファイルから読み込んだ スナップショットに追加します
挿入されたスナップショットを処理した後 更新の処理を行うために ファイルからのスナップショットを saveのリクエスト内の スナップショットに置き換えます 最後に ファイルから読み込んだ スナップショットから 削除済みのスナップショットを削除します これで snapshotsByIdentifier辞書の データの更新が完了しました これを元のファイルに書き込みます
JSONEncoderを使用して 複数あるスナップショットの 作業用コピーをディスク上の 1つのJSONファイルに書き込みます 最後に saveの結果とともに DataStoreSaveChangesResultを 返します DataStoreSaveChangesResultには 更新するコンテキストの remappedPersistentIdentifiersが 含まれます
カスタムデータストアが完成したので SampleTripsに導入できます アプリの定義で ストアの型を変更できます それには ModelConfigurationを JSONStoreConfigurationに 置き換えるだけです この1か所を置き換えるだけで 別のストア型を使用することが ModelContainerで認識されます SampleTripsアプリの モデルやビューのコードを 変更する必要はありません
DataStoreを使用することにより SwiftDataで任意の保存形式や 永続性バックエンドのデータの 読み書きができます
これにより 必要に応じて 任意のドキュメント データベース クラウドストレージに SwiftUIや 永続性モデルを活用できるとともに ModelContextが フィルタやソートの機能を提供するため シンプルにストアを実装することができ 複雑さが軽減されます
SwiftDataでのカスタムストアの導入は DataStoreConfigurationを 変更するだけで 簡単です 新しいDataStoreプロトコルが 導入されたことにより 任意の永続性バックエンドの サポートを実装できます これにより SwiftDataの 新たな可能性が広がります 「What's New in SwiftData」では インデックスや一意制約などの 新機能を紹介しています また「Track model changes with SwiftData History」では ストアの履歴を確認する方法を 紹介していますので ぜひご覧ください
ご視聴ありがとうございました 皆さんの成果に期待しています
-
-
8:15 - Implement a JSON store
// Implement a JSON store @available(swift 5.9) @available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) final class JSONStoreConfiguration: DataStoreConfiguration { typealias StoreType = JSONStore var name: String var schema: Schema? var fileURL: URL init(name: String, schema: Schema? = nil, fileURL: URL) { self.name = name self.schema = schema self.fileURL = fileURL } static func == (lhs: JSONStoreConfiguration, rhs: JSONStoreConfiguration) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } @available(swift 5.9) @available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) final class JSONStore: DataStore { typealias Configuration = JSONStoreConfiguration typealias Snapshot = DefaultSnapshot var configuration: JSONStoreConfiguration var name: String var schema: Schema var identifier: String init(_ configuration: JSONStoreConfiguration, migrationPlan: (any SchemaMigrationPlan.Type)?) throws { self.configuration = configuration self.name = configuration.name self.schema = configuration.schema! self.identifier = configuration.fileURL.lastPathComponent } func save(_ request: DataStoreSaveChangesRequest<DefaultSnapshot>) throws -> DataStoreSaveChangesResult<DefaultSnapshot> { var remappedIdentifiers = [PersistentIdentifier: PersistentIdentifier]() var serializedTrips = try self.read() for snapshot in request.inserted { let permanentIdentifier = try PersistentIdentifier.identifier(for: identifier, entityName: snapshot.persistentIdentifier.entityName, primaryKey: UUID()) let permanentSnapshot = snapshot.copy(persistentIdentifier: permanentIdentifier) serializedTrips[permanentIdentifier] = permanentSnapshot remappedIdentifiers[snapshot.persistentIdentifier] = permanentIdentifier } for snapshot in request.updated { serializedTrips[snapshot.persistentIdentifier] = snapshot } for snapshot in request.deleted { serializedTrips[snapshot.persistentIdentifier] = nil } try self.write(serializedTrips) return DataStoreSaveChangesResult<DefaultSnapshot>(for: self.identifier, remappedPersistentIdentifiers: remappedIdentifiers, deletedIdentifiers: request.deleted.map({ $0.persistentIdentifier })) } func fetch<T>(_ request: DataStoreFetchRequest<T>) throws -> DataStoreFetchResult<T, DefaultSnapshot> where T : PersistentModel { if request.descriptor.predicate != nil { throw DataStoreError.preferInMemoryFilter } else if request.descriptor.sortBy.count > 0 { throw DataStoreError.preferInMemorySort } let objs = try self.read() let snapshots = objs.values.map({ $0 }) return DataStoreFetchResult(descriptor: request.descriptor, fetchedSnapshots: snapshots, relatedSnapshots: objs) } func read() throws -> [PersistentIdentifier: DefaultSnapshot] { if FileManager.default.fileExists(atPath: configuration.fileURL.path(percentEncoded: false)) { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let trips = try decoder.decode([DefaultSnapshot].self, from: try Data(contentsOf: configuration.fileURL)) var result = [PersistentIdentifier: DefaultSnapshot]() trips.forEach { s in result[s.persistentIdentifier] = s } return result } else { return [:] } } func write(_ trips: [PersistentIdentifier: DefaultSnapshot]) throws { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let jsonData = try encoder.encode(trips.values.map({ $0 })) try jsonData.write(to: configuration.fileURL) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。