-
SwiftDataの新機能
SwiftDataの最新の機能強化を紹介します。Codableを使ってカスタム型やサードパーティの型を永続化する方法や、取得したデータをSwiftUIアプリのセクションにグループ化する方法を確認します。また、ModelResultsObserverやHistoryObserverを使用して、あらゆる場所でのデータストアの変更をオブザーブする方法も学びます。この方法により、パワフルな状態オブジェクトの利用、デリゲートベースのアーキテクチャとの統合、モデルの更新に対して正確に反応できる柔軟性を実現できます。
関連する章
- 0:00 - Introduction
- 0:53 - Sectioning your fetches
- 2:56 - Using custom types
- 6:26 - Observing data stores with ResultsObserver
- 9:41 - Observing history with HistoryObserver
- 12:20 - Next steps
リソース
関連ビデオ
WWDC26
WWDC24
-
このビデオを検索
こんにちは、私はThomasです。SwiftDataチームの エンジニアです。 このセッションでは、SwiftDataの 新しい機能をいくつかご紹介します。 Appleの2027年リリースに含まれる 新機能です。 SwiftDataを始めたばかりの方は、コードアロング 「Add persistence with SwiftData」をぜひご覧ください。 新しいアプリでSwiftDataを 採用する方法を学べます。 Appleの2027年リリースでは、 SwiftDataに魅力的な新機能が追加されます。 まず、QueryをSwiftUIビューで 使う方法をご紹介します。 データをセクションごとに 取得する方法です。 次に、モデルにカスタム型を 保存する際の機能強化について説明します。 カスタム型の保存方法について 詳しく見ていきます。 最後に、素晴らしい新しいAPIを いくつかご紹介します。 SwiftDataストアにおけるモデルと 履歴変更を観察するためのAPIです。 セクション機能を見てみましょう。 ここ数年、「SampleTrips」というアプリを 開発してきました。計画している
すべての旅行を記録できるアプリです。 SampleTripsは永続化にSwiftData、 ユーザーインターフェースにSwiftUIを使用しています。 SwiftUIとSwiftDataは 非常に相性が良いです。 SwiftUIでSwiftDataから読み込むのは、 ビューにQueryを追加するだけで簡単です。 ここでQueryは、開始日でソートされた すべての旅行をSwiftDataから取得します。 このクエリの結果は、ビュー本体で使用して 旅行のリストを作成できます。 SampleTripsで、旅行を 目的地ごとにグループ化するオプションを追加したいと思います。 同じ場所への旅行が複数ある場合に、 それらを並べて確認できるようにします。 Appleの2027年リリースでは、 Queryにセクション機能のサポートが追加されます。 セクション付きクエリを作成するには、 KeyPathを渡します。 Tripモデルのルートから Queryの新しいsectionByパラメーターに 文字列へのKeyPathを渡します。 ここでは、Tripsのdestinationプロパティへの KeyPathを使用してセクションを作成します。 各目的地ごとにセクションを作成します。 Queryのラップされた値は 引き続きTripの配列です。 そのため、現時点ではリストは 以前と同じように見えます。 リストをセクション分けするために、 現在のForEachをセクションでラップして 2つ目のForEachを追加します。 セクションにアクセスするには、プロパティラッパーから クエリにアクセスします。 アンダースコア付きの名前 「_trips」を使用します。 クエリにはsectionsプロパティがあり、 セクションのリストを返します。 そして、SwiftUIのForEachを使用して すべてのセクションを反復処理できます。 各セクションにはIDプロパティがあります。 IDは、KeyPathから取得した モデルの値です。 QueryのSectionByパラメーターに 渡したKeyPathです。 そのため、TripListViewでは IDはセクション内のすべての旅行の 目的地になります。
IDをセクションのヘッダーラベルとして使用します。 セクション自体は旅行のコレクションです。 内側のForEachを変更して セクション内の各旅行を反復処理し、 TripListItemを作成します。 これで、目的地ごとにグループ化された すべての旅行を確認できます。 次のセクションに進みましょう。 モデルにカスタム型を保存する 機能強化について 詳しく見ていきます。 SampleTripsアプリの中核となるのが Tripモデルです。 名前、目的地、開始日と終了日などの 情報を保存します。 現在、目的地はTripモデルに 文字列として保存されています。 MapKitから場所を検索できる 機能を追加したいと思います。 MapKitの検索APIを使用して 目的地を検索できるピッカーを アプリ内にすでに構築しました。 各場所について、MapKit APIは名前、 地理座標、MKMapItem.Identifierなど 多くのメタデータを提供します。 このMKMapItem.IdentifierはMapKitで 場所を一意に識別し、 場所に関するより多くのメタデータを 検索するために使用できます。 また、マップアプリで場所を 開くためにも使用できます。 座標を保存したいと思います。 そしてMKMapItem.Identifierを 旅行モデルに保存します。 Tripクラスにそれらを追加します。 しかし、アプリを起動すると、 SwiftDataがFatal Errorを発生させます。 「Class property within Persisted Struct/Enum is not supported」というエラーで、 MKMapItem.Identifierが 原因として指摘されています。
SwiftDataが読み込まれると、 モデルのスキーマを自動的に生成します。 スキーマは、モデルクラスとエンティティの間の マッピングを定義します。 DataStoreが永続化できる プロパティとのマッピングです。 ほとんどの型では これは自動的に機能します。 ただし、Modelマクロでアノテーションされていない クラスは自動的に 検査できず、SwiftDataが スキーマの生成に失敗します。 MKMapItem.IdentifierはMapKitから 来ているため、 実装にアクセスできません。 @Modelとしてマークするために変更できません。 SwiftDataも検査できません。 クラスだからです。 ただし、Codableに準拠しています。 Codable型は、データへの シリアライズ方法を知っています。 そしてデータからインスタンス化することも できます。 Appleの2027年リリースでは、 モデル属性を.codableとしてマークできます。 これにより、SwiftDataに型への シリアライズを委任するよう指示します。 そしてシリアライズされた表現を 保存します。 mapItemIdentifier属性を codableとしてマークしましょう。 これで、SampleTripsはエラーなしで 起動します。 素晴らしいですね。 codable属性は、SwiftDataにエンコードされた 表現を永続化するよう指示します。 型のスキーマを推論する代わりに 使用します。 transformable属性と同様に、codableには いくつかの注意点があります。 codable属性の内容は SwiftDataに対して不透明です。 これは、Predicatesで結果を フィルタリングするために使用できないことを意味します。 またはSort Descriptorsを使った ソートにも使用できません。 また、codable型の形状が変わった場合、 プロパティの追加や削除など、 マイグレーションはトリガーされません。 型のCodable実装は、エンコードと デコードを行えなければなりません。 前方互換性と後方互換性を 保った方法で行う必要があります。
Codable属性の使用は、SwiftDataが ネイティブにサポートしない型を永続化するための 「抜け穴」と考えることができます。 自分で定義した型には codableの使用を避けるべきです。 SwiftDataモデルまたはサポートされた値型として モデリングすることで、 ソート、フィルタリング、インデックスなど SwiftDataの全機能を活用できます。 しかし、自分が所有していない型の場合、 codableを使うことで他の属性と 一緒に保存できます。 これで、SampleTripsでMapKitから 場所を選択して永続化できます。 目的地のMKMapItem.Identifierを 旅行モデルの他のプロパティと 一緒に保存できます。 では、ストアを監視して データ変更時に通知を受け取る方法を 見てみましょう。 以前、TripListViewでqueryマクロを使用して 旅行を SwiftDataストアからSwiftUIで 利用できるようにしました。 @Queryは強力なツールです。 ビューが表示されると、モデルを取得します。 SwiftDataストアから ビュー本体で使用できるようにします。 そして、結果に影響を与える変更を 継続的にストアで監視します。 例えば、旅行がストアから 削除された場合、 Queryは変更を検知します。 そして、ビューはクエリの新しい結果を 反映して再レンダリングされます。 Queryは優れており、SwiftUIビューでは 最初の選択肢であるべきです。 しかし、アプリのSwiftUI以外の部分は どうでしょうか。 SwiftDataストアから値を導出する 状態オブジェクトがあり、 データが変更されたときに 再計算が必要かもしれません。 あるいは、SceneKitで書かれたゲームのように アプリがSwiftUIをまったく使用していないかもしれません。
Appleの2027年リリースでは、 ResultsObserverを導入します。 SwiftUIビューのQueryと同様に、 ResultsObserverはデータを取得します。 SwiftDataストアから取得し、 ストアの変更を監視します。 ただし、アプリのどこでも動作します。 SwiftUIビューに依存せずに、 Swift Observationを使用します。 すでに知っているのと同じクエリプリミティブを サポートします。 フィルタリング、ソート、そして 先ほど説明したセクション機能です。 先ほど共有したデータフロー図です。 ただし、QueryをResultsObserverに 置き換え、Viewを好きなコードに 置き換えたものです。 ResultsObserverはデータを取得し、 ストアの変更を監視します。 コードはSwift Observationを使用して これらの変更に対応できます。 先ほど、SampleTripsアプリに 位置情報を追加しました。 旅行を計画しているすべての場所を 表示するマップを追加したいと思います。 SwiftUIのMapKit統合を使用しています。 ただし、マップに表示される エリアをカスタマイズしたいと思います。 これを実現するために、新しい MapCameraControllerを追加しました。 マップに適したMapCameraBoundsを 計算します。 ResultsObserverを使用して、 MapCameraBoundsを再計算するタイミングを把握します。 これを行うために、旅行用の ResultsObserverを作成します。 マップでは、常にすべての旅行を 表示したいため、 フィルタリング用のpredicateや セクション用のkey pathは渡しません。 次に、didSetオプションを使って withContinuousObservationを使用します。 旅行が変更されるたびに コールバックを受け取ります。 結果が変更されたとき、 マップの範囲を再計算します。 withContinuousObservationは ObservationTrackingトークンを返します。 このトークンは 観察のライフタイムを定義します。 このトークンをクラスに保存して 更新を受け取ります。 MapCameraControllerの ライフタイム全体にわたって受け取ります。 実際の動作を見てみましょう。 SampleTripsを起動すると、 MapCameraControllerが すべての旅行をビューに収めた マップカメラの範囲を選択します。 残念ながら、今月後半に トロントを訪れることができないので、 この旅行を削除します。
ResultsObserverは変更を検知し、 MapCameraControllerが マップのカメラの範囲を 再計算します。 ResultsObserverは、コードがデータストアの 変更に対応する必要がある場合に 強力なツールです。アプリのどこでも使えます。 しかし、そこで止まりませんでした。 Appleの2027年リリースでは、 履歴の観察もサポートされています。 まず、簡単な履歴の説明をします。 ストア内のデータが変更されると、SwiftDataは 変更されたすべての内容を記録します。 この履歴を使用して機能を 構築できます。 外部サーバーとの同期や、 アプリの外部で行われた変更への 対応など、 アプリ拡張機能での変更など。 データストアが保存されるたびに、 SwiftDataは履歴トランザクションを記録します。 履歴トランザクションには 変更内容に関する情報が含まれます。 変更がどこから来たかについての情報と、 履歴内でトランザクションを 一意に識別するトークンが含まれます。 このトークンは ModelContext.fetchHistory APIと共に使用できます。 新しいトランザクションを取得するために 使用します。 永続的な履歴について詳しく知りたい方は、 WWDC 2024の「Track Model Changes with SwiftData History」をご覧ください。 Appleの2027年リリースの新機能として、 HistoryObserverが追加されます。 HistoryObserverを使うと、ストアの変更に 簡単に対応できます。 ResultsObserverがフェッチ結果を観察するのと 同様に、HistoryObserverは 永続的な履歴を観察し、新しいトランザクションが 追加されたときにコードが対応できるようにします。
特定の種類の変更のみを 知りたい場合は、 HistoryObserverを使用して、モデルタイプと トランザクション作成者でフィルタリングできます。 履歴の観察は便利です。 アプリがデータストアの一部を 同期させる必要がある場合に、 外部サーバーなどの 他のシステムと同期させる場合です。 HistoryObserverには単一の 観察可能なプロパティがあります。eventCounterです。 永続的な履歴で新しいトランザクションが 利用可能になると、 eventCounterがインクリメントされます。 コードはeventCounterを観察し、 インクリメントされたときに、 ModelContext.fetchHistory APIを使用して 最新の変更を取得できます。
HistoryObserverを使用して変更を同期する 方法の例を示します。 アプリで行われた変更を 外部サーバーと同期します。 まず、modelContainer用の HistoryObserverを設定します。 この場合、アプリによる変更のみに 関心があるため、 authorsとして「App」を渡しています。 サーバーから来た変更を サーバーに再送信しないようにするためです。 次に、withContinuousObservationを使用します。 MapCameraControllerと同様に、 観察トラッキングトークンを クラスに保存します。 クラスのライフタイム全体にわたって トラッキングがアクティブな状態を維持します。 観察クロージャ内で eventCounterにアクセスする必要があります。 Swift Observationが何をトラックするかを 認識できるようにします。 最後に、processChanges関数を呼び出します。 processChangesでは、 ModelContext.fetchHistory APIを使用して 履歴を取得し、変更を サーバーにアップロードできます。 私と同じくらい皆さんも ワクワクしていただけると嬉しいです。 2027年リリースのSwiftDataの 新機能についてです。 セクションを使ったSwiftDataフェッチの カスタマイズ。 他のフレームワークからの型を保存する際の codable属性の使用。 SwiftUIビューの外でクエリ結果の変更に 対応するためのResultsObserverの使用。 そして最後に、 SwiftDataの永続的な履歴の変更に 対応するためのHistoryObserverの使用。 ご視聴ありがとうございました。
-
-
0:01 - Sectioned fetching
// Sectioned fetching struct TripListView: View { @Query(sort: \Trip.startDate, sectionBy: \.destination) var trips: [Trip] var body: some View: { List(selection: $selection) { ForEach(_trips.sections) {section in Section(section.id) { ForEach(trips) {trip in TripListItem(trip: trip) } } } } } } -
4:59 - Using Codable
// Using Codable import SwiftData @Model class Trip { struct Location: Codable { var latitude: Double var longitude: Double } var name: String var destination: String var startDate: Date var endDate: Date var location: Location? @Attribute(.codable) var mapItemIdentifier: MKMapItem.Identifier? } -
8:20 - // Use observation to update map bounds
// Use observation to update map bounds @Observable @MainActor final class MapCameraController { private let resultsObserver: ResultsObserver<Trip, Never> var bounds: MapCameraBounds? private var token: ObservationTracking.Token? init(modelContext: ModelContext) throws { resultsObserver = try ResultsObserver<Trip, Never>(modelContext: modelContext) token = withContinuousObservation(options: [.didSet]) {[weak self], event in self?.bounds = self?.calculateBounds(trips: resultsObserver.results) } } private func calculateBounds(trips: [Trip]) -> MapCameraBounds? { / * */ } } -
8:21 - // Using HistoryObserver to sync with a server
// Using HistoryObserver to sync with a server @SyncActor final class ServerSync { private let observer: HistoryObserver private var token: ObservationTracking.Token? func start() throws { self.observer = try HistoryObserver(authors: ["App"], modelContainer: modelContainer) token = withContinuousObservation(options: .didSet) {[weak self] _ in _ = self?.observer.eventCounter self?.processChanges() } } private func processChanges() { // Fetch and process history transactions } }
-
-
- 0:00 - Introduction
What's new in SwiftData for Apple's 2027 releases, with a preview of the agenda: sectioning fetches, using custom types, and observing data stores.
- 0:53 - Sectioning your fetches
Use @Query's new sectionBy: parameter to group results by a key path. Access the underlying query via the underscore-prefixed name to iterate sections, each with an id and a collection of models — demonstrated by grouping trips by destination in SampleTrips.
- 2:56 - Using custom types
Store types SwiftData can't model natively — like MKMapItem.Identifier — by marking properties with @Attribute(.codable). Treat codable as an escape hatch for external types, not types you own; modeling your own types as @Model or supported value types unlocks filtering, sorting, and migration.
- 6:26 - Observing data stores with ResultsObserver
The new ResultsObserver brings Query-style fetching to non-SwiftUI code. Combine it with withContinuousObservation(didSet:) to react to model changes anywhere in your app — shown updating map camera bounds as trips change.
- 9:41 - Observing history with HistoryObserver
HistoryObserver exposes a single observable eventCounter that ticks when new persistent-history transactions arrive. Pair it with ModelContext.fetchHistory() to filter by model type or transaction author — ideal for syncing changes to an external server.
- 12:20 - Next steps
Recap: section your fetches, adopt codable types, react to data changes with ResultsObserver, and observe history with HistoryObserver.