WWDC Quick Look 💓 By SwiftGGTeam
SwiftData の新機能

SwiftData の新機能

元の動画を見る

ハイライト

SwiftData には 3 つの機能が追加されます。@QuerysectionBy によるプロパティ単位のグループ化に対応し、@Attribute(.codable) はサードパーティフレームワークの型を永続化でき、ModelResultsObserverHistoryObserver によって SwiftUI 以外のコードでもデータ変更を観測できます。

主要内容

グループ化リストに手書きの Dictionary はもういらない

グループ化リストを作ったことがある開発者なら、この流れを経験しているはずです。まず SwiftData から全データを取得し、あるプロパティで手動で Dictionary にグループ化し、最後にその辞書を Section に変換して List に入れる。コードは短くなく、データが変わるたびにグループ化を再計算する必要があります。

(00:36) Apple は @QuerysectionBy 引数を追加しました。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) @QuerysectionBy 引数は 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: \.destinationdestination プロパティでグループ化するよう SwiftData に伝える
  • _trips はプロパティラッパーの下層インスタンスで、これを通じて sections にアクセスする
  • sectionid はグループ化キーの値で、ここでは Section の header として使う
  • 内側の ForEachsection 自体を走査する。これは 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 が必要
  • predicatesortsectionBy に対応し、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 とデータの結合が下がり、テストしやすくなります。始め方: @QueryModelResultsObserver に置き換え、withContinuousObservation で変更を受け取り、@Observable を通じて View に通知します。

2. リアルタイム共同編集ノートアプリを作る

HistoryObserverauthors フィルタと transaction token は、複数端末同期に自然に合います。何をするか: ローカル編集で発生した transaction を "アプリ"、サーバーから届いた transaction を "サーバー" とマークし、HistoryObserver"アプリ" の変更だけを監視してアップロードします。なぜ価値があるか: 変更追跡を自分で実装する必要がなく、SwiftData がすでに記録しています。始め方: HistoryObserver 初期化時に authors: ["アプリ"] を渡し、processChanges 内で fetchHistory を呼んで transaction 一覧を取得します。

3. SpriteKit/SceneKit ゲームで SwiftData に進捗を保存する

ゲームは通常 SwiftUI を使わないため、以前は SwiftData のリアルタイム観測機能を活用しにくい状況でした。何をするか: ModelResultsObserver でプレイヤーデータの変化を監視し、ゲーム状態を動的に調整します。なぜ価値があるか: SwiftData を使うためだけにゲームアーキテクチャへ SwiftUI を無理に入れる必要がありません。始め方: ゲームの GameManagerModelResultsObserver<PlayerData> を持たせ、データ変化時にゲーム内変数を更新します。

4. 日付でグループ化した予定/ToDo リスト

sectionBy によって日付ごとのグループ化は trivial になります。何をするか: 予定アプリでイベントを日別に表示します。なぜ価値があるか: 以前は Calendar を使った手動グループ化が必要でしたが、今は sectionBy 1 行で済みます。始め方: @Query(sort: \.startDate, sectionBy: \.dateString) と書き、dateString は整形済み日付文字列を返す計算プロパティにします。

5. モデルに複雑な設定オブジェクトを直接保存する

一部のサードパーティ SDK の設定型は Codable に準拠しているものの、構造が複雑です。何をするか: @Attribute(.codable) で SwiftData に直接保存し、基本型へ手動分解しないようにします。なぜ価値があるか: 変換コードを省き、モデルをきれいに保てます。始め方: 型が Codable に準拠していることを確認し、@Attribute(.codable) を付けます。

関連 Session

コメント

GitHub Issues · utterances