ハイライト
SwiftData には 3 つの機能が追加されます。
@QueryはsectionByによるプロパティ単位のグループ化に対応し、@Attribute(.codable)はサードパーティフレームワークの型を永続化でき、ModelResultsObserverとHistoryObserverによって SwiftUI 以外のコードでもデータ変更を観測できます。
主要内容
グループ化リストに手書きの Dictionary はもういらない
グループ化リストを作ったことがある開発者なら、この流れを経験しているはずです。まず SwiftData から全データを取得し、あるプロパティで手動で Dictionary にグループ化し、最後にその辞書を Section に変換して List に入れる。コードは短くなく、データが変わるたびにグループ化を再計算する必要があります。
(00:36) Apple は @Query に sectionBy 引数を追加しました。KeyPath を渡すだけで、SwiftData はクエリ層で直接グループ化します。ビュー側に必要なのは 2 階層の ForEach だけです。外側は sections を走査し、内側は各 section 内のデータを走査します。
サードパーティ型にようやく逃げ道ができた
モデルに MKMapItem.Identifier を保存したい場合はどうでしょう。以前の SwiftData は fatal error「Persisted Struct/Enum 内の Class プロパティはサポートされていません」を直接投げていました。MKMapItem.Identifier は MapKit の class であり、ソースコードに @Model を付けられず、SwiftData も中身を inspect できないからです。
(04:52) 新しい @Attribute(.codable) は、SwiftData がシリアライズを型自身に委譲できるようにします。型が Codable に準拠していれば、SwiftData は schema を生成しようとせず、エンコード済みの結果を保存します。
代償もあります。Codable 属性は SwiftData から見ると不透明です。Predicate フィルタや並べ替えには使えません。型構造が変わってもマイグレーションは発生しません。Apple はこれを明確に “escape hatch” と呼び、自分で制御できないサードパーティ型だけに使うべきだと説明しています。自分で書くモデルには、引き続き @Model や値型を使うべきです。
データ観測が SwiftUI の外へ出る
@Query は SwiftUI ビュー内でしか使えません。アプリに独立した状態オブジェクトがあり、SwiftData からデータを取得し、変更に反応する必要がある場合、以前は workaround が必要でした。見えない SwiftUI ビューを隠しておくか、自分で NotificationCenter を受けるかです。
(07:21) ModelResultsObserver は @Query の能力を外へ取り出します。フィルタ、並べ替え、グループ化をサポートし、Swift Observation フレームワークの withContinuousObservation と組み合わせることで、任意の Swift コードがデータ変更通知を受け取れます。
(09:30) HistoryObserver はさらに踏み込みます。SwiftData は保存のたびに history transaction を記録し、何が変わったか、誰が変えたか、一意の token を含めます。HistoryObserver はこれらの transaction を監視し、サーバー同期や app extension による変更への反応に向いています。作者でフィルタすれば、サーバーから取得した変更を再度アップロードしてしまうことを避けられます。
詳細
Sectioned Fetching
(00:01) @Query の sectionBy 引数は KeyPath を受け取り、そのプロパティ値でクエリ結果をグループ化します。wrapped value は引き続き [Trip] ですが、アンダースコア付きプロパティから下層の Query オブジェクトにアクセスし、sections コレクションを取得できます。
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(section) { trip in
TripListItem(trip: trip)
}
}
}
}
}
}
ポイント:
sectionBy: \.destinationはdestinationプロパティでグループ化するよう SwiftData に伝える_tripsはプロパティラッパーの下層インスタンスで、これを通じてsectionsにアクセスする- 各
sectionのidはグループ化キーの値で、ここではSectionの header として使う - 内側の
ForEachはsection自体を走査する。これはRandomAccessCollectionである
Codable 属性値
(04:59) @Attribute(.codable) は、永続化を型の Codable 実装に委ねます。SwiftData はこの種のプロパティには schema を生成せず、エンコード済みデータを保存します。
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?
}
ポイント:
MKMapItem.Identifierは MapKit の class で、SwiftData は自動的に inspect できないCodableに準拠しているため、.codable属性でマークできる- SwiftData が保存するのはエンコード済みのバイナリまたは JSON で、内部構造は不透明
#PredicateのフィルタやSortDescriptorの並べ替えには使えない- 型構造の変更は自動マイグレーションを発生させないため、前方後方互換なエンコードロジックを保つ必要がある
ModelResultsObserver
(08:20) ModelResultsObserver は、SwiftUI 以外のコードで @Query 相当の能力を提供します。データをクエリし、変化を継続的に観測できます。
@Observable @MainActor final class MapCameraController {
private let observer: ModelResultsObserver<Trip>
var bounds: MapCameraBounds?
private var token: ObservationTracking.Token?
init(modelContext: ModelContext) throws {
observer = try ModelResultsObserver<Trip>(modelContext: modelContext)
token = withContinuousObservation(options: [.didSet]) { [weak self] _ in
self?.bounds = self?.calculateBounds(trips: observer.results)
}
}
private func calculateBounds(trips: [Trip]) -> MapCameraBounds? { /* ... */ }
}
ポイント:
ModelResultsObserverの初期化にはModelContextが必要predicate、sort、sectionByに対応し、API は@Queryと一貫しているwithContinuousObservationが返すtokenは強参照で保持する必要がある。保持しないと観測はすぐに無効になる- クロージャ内で
observer.resultsを読む必要があり、Observation フレームワークはプロパティアクセスを通じて依存関係を追跡する [weak self]は循環参照を避ける
HistoryObserver
(08:21) HistoryObserver は SwiftData の永続化 history transaction を監視し、サーバー同期のような場面に向いています。
@SyncActor final class ServerSync {
private let observer: HistoryObserver
private var token: ObservationTracking.Token?
func start() throws {
observer = try HistoryObserver(authors: ["アプリ"], modelContainer: modelContainer)
token = withContinuousObservation(options: .didSet) { [weak self] _ in
_ = self?.observer.eventCounter
self?.processChanges()
}
}
private func processChanges() {
// ModelContext.fetchHistory でトランザクションを取得し、サーバーへアップロードする。
}
}
ポイント:
authors引数は transaction の発生元をフィルタし、サーバー同期で取得した変更を再度アップロードすることを防ぐeventCounterは唯一の観測可能プロパティで、新しい transaction が来るたびに増える- クロージャ内で
eventCounterにアクセスする必要があり、それによって Observation フレームワークが依存関係を作る - 実際の処理では
ModelContext.fetchHistoryを呼び出して transaction の詳細を取得する - 各 transaction には一意の token があり、処理済み変更の追跡に使える
重要な示唆
1. データ層をビュー層から完全に切り離す
これまで MVVM アーキテクチャでは、ViewModel が SwiftData の変更を観測したい場合に回り道が必要でした。今は ModelResultsObserver によって、ViewModel 自身がデータ観測を持ち、View は描画だけに集中できます。何をするか: 既存プロジェクトをリファクタリングし、@Query を ViewModel から外します。なぜ価値があるか: View とデータの結合が下がり、テストしやすくなります。始め方: @Query を ModelResultsObserver に置き換え、withContinuousObservation で変更を受け取り、@Observable を通じて View に通知します。
2. リアルタイム共同編集ノートアプリを作る
HistoryObserver の authors フィルタと transaction token は、複数端末同期に自然に合います。何をするか: ローカル編集で発生した transaction を "アプリ"、サーバーから届いた transaction を "サーバー" とマークし、HistoryObserver は "アプリ" の変更だけを監視してアップロードします。なぜ価値があるか: 変更追跡を自分で実装する必要がなく、SwiftData がすでに記録しています。始め方: HistoryObserver 初期化時に authors: ["アプリ"] を渡し、processChanges 内で fetchHistory を呼んで transaction 一覧を取得します。
3. SpriteKit/SceneKit ゲームで SwiftData に進捗を保存する
ゲームは通常 SwiftUI を使わないため、以前は SwiftData のリアルタイム観測機能を活用しにくい状況でした。何をするか: ModelResultsObserver でプレイヤーデータの変化を監視し、ゲーム状態を動的に調整します。なぜ価値があるか: SwiftData を使うためだけにゲームアーキテクチャへ SwiftUI を無理に入れる必要がありません。始め方: ゲームの GameManager に ModelResultsObserver<PlayerData> を持たせ、データ変化時にゲーム内変数を更新します。
4. 日付でグループ化した予定/ToDo リスト
sectionBy によって日付ごとのグループ化は trivial になります。何をするか: 予定アプリでイベントを日別に表示します。なぜ価値があるか: 以前は Calendar を使った手動グループ化が必要でしたが、今は sectionBy 1 行で済みます。始め方: @Query(sort: \.startDate, sectionBy: \.dateString) と書き、dateString は整形済み日付文字列を返す計算プロパティにします。
5. モデルに複雑な設定オブジェクトを直接保存する
一部のサードパーティ SDK の設定型は Codable に準拠しているものの、構造が複雑です。何をするか: @Attribute(.codable) で SwiftData に直接保存し、基本型へ手動分解しないようにします。なぜ価値があるか: 変換コードを省き、モデルをきれいに保てます。始め方: 型が Codable に準拠していることを確認し、@Attribute(.codable) を付けます。
関連 Session
- SwiftUI の新機能 — SwiftUI の新機能と SwiftData の
@Queryとの深い統合 - SwiftData persistence の新機能 — SwiftData 永続化層の低レベルな改善
- Track Model Changes with SwiftData History — 永続化 history transaction の詳細な仕組み (WWDC 2024)
- Add persistence with SwiftData — SwiftData 入門 code-along
- Swift の新機能 — Observation フレームワークの改善を含む Swift 言語の新機能
コメント
GitHub Issues · utterances