View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

その他のビデオ

  • 概要
  • トランスクリプト
  • コード
  • SwiftData:継承とスキーマの移行の詳細

    クラス継承を使ってデータをモデリングする方法を学びましょう。クエリを最適化し、継承を使用するためにアプリのデータをシームレスに移行する方法について説明します。モデルグラフの構築、効率的なフェッチとクエリの作成、堅牢なスキーマ移行の実装のためのサブクラスについて解説します。また、Observableと永続的な履歴を使って、変更を効率的に管理する方法も紹介します。

    関連する章

    • 0:00 - イントロダクション
    • 2:11 - クラス継承の活用
    • 7:39 - 移行によるデータ利用の進化
    • 11:27 - フェッチしたデータの調整
    • 13:54 - データへの変更の監視
    • 18:28 - 次のステップ

    リソース

    • Adopting SwiftData for a Core Data app
    • Building rich SwiftUI text experiences
    • SwiftData
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC25

    • SwiftUIの新機能

    WWDC24

    • SwiftDataの履歴機能によるモデル変更のトラッキング
  • このビデオを検索

    こんにちは SwiftDataチームの エンジニアのRishi Vermaです 「SwiftData: 継承とスキーマの移行の詳細」へ ようこそ iOS 17で導入されたSwiftDataでは Swift内のアプリのデータを Appleのすべてのプラットフォームで モデル化して永続化することができます Swift言語の 最新機能を利用して 高速で効率的かつ安全な コードを記述できます その続きとして このビデオではクラス継承を 利用する方法を紹介し どのような場合に 継承が最適であるか説明します 継承の導入や スキーマの進化を利用した データを保持するための 移行方法について説明します そのあと SwiftDataのフェッチやクエリを 調整してパフォーマンスを最適化する 方法をいくつか見ていきます 最後に ローカルおよびリモートで行われた モデルへの変更を監視する方法を説明します これまでのいくつかのリリースでは おなじみのSampleTripsを使用していました これはSwiftUIで記述されたアプリで 計画したすべての旅行を 記録することができます このアプリのモデルで SwiftDataを使用するには フレームワークをインポートし

    各モデルに Modelマクロを追加するだけです

    アプリの定義に移動して WindowGroupで modelContainerモディファイアを追加し ビュー階層全体に Tripモデルを通知します modelContainerを接続したら ビューを更新して Queryマクロを利用できるようになります 静的なデータを削除し 代わりにモデルコンテナから 旅行をフェッチするための コードを生成する Queryマクロを使用して ビューを入力していきます

    以上です これでアプリは作成した旅行を すべて永続化し SwiftUIビューにぴったり 収めることができます SwiftDataでは 簡単に永続化できるだけではなく スキーマのモデル化と移行や グラフの管理 CloudKitとの同期など 多数の機能が提供されています そして最近 SwiftDataに 新たに追加された機能が クラス継承です iOS 26では 新機能として 継承を利用したモデルグラフを 作成できるようになりました クラス継承は強力なツールです どのような場合に有効か 調べてみましょう 継承は モデルが自然階層をなしており 共通の特性を持っている場合に うまく機能します Tripモデルには すべての旅行で必要となる destination startDate endDateなどの プロパティがあり いつどこに行くのかがわかります このため Tripの新しいサブクラスには これらのプロパティや Tripで定義されたその他の共通の動作が 事前に含められています Tripモデルは幅広いドメインでもあります 旅行には非常に多くの種類があります Tripの新しいサブクラスは より幅広いTripドメインに収まる 自然サブドメインである必要があります

    SampleTripsアプリでは 多くの旅行が 個人旅行とビジネス旅行の 2つの自然サブドメインに分類されます 旅行の自然サブドメインを表現する これら2つの新しいモデルを用意したら これらのサブクラスに固有の プロパティと動作を追加します 個人旅行については その旅行に行くかもしれない理由を キャプチャする 列挙を追加します ビジネス旅行については 日給を記録するためのプロパティを追加します 今度の出張に費やすべき費用が わかるようになります これをSampleTripsアプリで行い 体験を向上させましょう これはTripクラスです これにはサブクラスと共有したい プロパティが含まれています ビジネス旅行と個人旅行に1つずつ 2つの新しいサブクラスを Tripsアプリに追加しましょう iOS 26以降では SwiftDataの継承サポートに合わせるために @availableも 追加する必要があります 次に サブドメインに固有の プロパティをサブクラスに追加しましょう BusinessTripにperdiemを追加し 初期値を設定します またPersonalTripに Reason列挙を追加し その個人旅行に出かける理由を キャプチャします 最後に 新しいサブクラスが含まれるように スキーマを変更する必要があります BusinessTripとPersonalTripを modelContainerモディファイアに追加します これで準備完了です SampleTripsアプリで その他の特別なコードを必要とせずに 新しい青色の個人旅行と緑色のビジネス旅行を 利用できるようになりました クラス継承は強力なツールですが すべての問題に通用するわけではありません 次に どのような場合に継承を利用できるか について説明します

    継承が有効である いくつかのシナリオがあります モデルが自然と階層関係を 表しており 拡張したい共通の特性が モデルにある場合 そのタイプは「is-a」関係になっているため 継承が有効である可能性があります

    継承したモデルを使用すると 個人旅行が旅行の一種だとわかります このビューの旅行のクエリのように Tripタイプを操作するときにはいつでも 個人旅行やビジネス旅行 さらには ただのTrip親クラスのインスタンスを含む あらゆるタイプの旅行を検索できます ここではUIと同じ色分けを使用した 旅行を表す飛行機が クエリの背後でモデルコンテナから モデルコンテキストに飛んでいます ただし 共通の特性を モデル間で共有する目的で 継承を 利用すべきではありません 例えば nameという名前のプロパティが含まれる すべてのモデルをサブクラス化した場合 共通の目的のために 1つのプロパティを共有する 多くのサブドメインが クラス階層に含まれるようになり その他のすべての特性が それらのサブドメインに隔離されてしまいます これらのサブドメインは 自然階層をなしていないため プロトコルの準拠として 表現した方がいいでしょう プロトコルの準拠を使用すると 異なるドメインで 動作を共有できますが 関係ないその他の特性は共有されません 継承を使用する別の理由は モデルのクエリまたはフェッチ方法によります

    データのクエリ方法はいくつかあります ここではQueryマクロを使用して モデルコンテナからすべての旅行をフェッチし ビューを生成しています これはディープ検索の一例です

    ディープ検索のみを使用する場合 つまり 常にすべての旅行をフェッチし Tripタイプのみを使用する場合は 個人旅行やビジネス旅行を サブクラスではなく Tripのプロパティとした方がいいでしょう クエリまたはフェッチが リーフクラスタイプのみをフェッチする場合 これをシャロー検索と呼びますが その場合 Tripはタイプとして クエリされたり利用されたりしないため モデルを平坦化できるかもしれません ただし ディープ検索と シャロー検索を使用する場合 すべての旅行またはPersonalTripsなどの 特定のサブタイプを頻繁に検索し そのタイプに合わせたビューを生成するため 継承が役立ちます ここで 個人旅行または ビジネス旅行のみが表示されるように Tripsアプリをどのように 変更できるか見てみましょう

    セグメントコントロールを利用して すべての旅行を表示したあと 特定のサブクラスを表示します

    選択したセグメントを使用して クラスが特定のタイプであるかを 「is」キーワードを用いて判定する プレディケートを作成できます 例えば ここでは PersonalTripかどうかを確認しています 次に プレディケートを提供し 旅行の開始日で並べ替えて クエリを初期化します アプリで確認してみましょう すべての旅行が表示された 旅行ビューからスタートします 次に ビューを特定のサブクラスに 絞り込みます 素晴らしいですね このようにしてiOS 26で クラス継承を活用できます とはいえ まだ終わりません ここまでスキーマにいくつかの 大きな変更を加えただけです これが既存のアプリにとって何を意味するか データをどのように移行できるか考えましょう SampleTripsアプリは 過去数回のリリースで いくつかの進化を遂げました ここでそれらすべてを バージョン管理されたスキーマと スキーマ移行計画でキャプチャし アップグレード時に アプリがユーザーのデータを 最新のSampleTripsアプリに 保持できるようにしましょう

    すべては最初のビデオから始まります そのビデオでは iOS 17のSwiftDataを紹介し Tripsを用いてその導入方法を紹介しました それらの紹介ビデオでは 旅行の名前を一意にする方法や 移行時にデータを保持するために プロパティの元の名前を 変更する方法について学びました

    iOS 17では 新しいバージョンID 2.0によって バージョン管理されたスキーマを作成し 変更したモデルTripを 一意の名前で表示し 開始日と終了日の名前を変更しました

    次に カスタム移行段階を追加し 既存のTripsを 重複排除できるようにしました そこでは ModelContextの fetch関数を利用して すべての旅行をフェッチし 重複排除できるようにしました

    iOS 18では IndexマクロとUniqueマクロを利用します また削除時に保持したいプロパティに マークを付けます

    これにより データストアから削除されたあとも モデルを識別できるようになります

    iOS 18のバージョン管理されたスキーマは バージョン3としてマークされ Tripモデルへの変更をキャプチャします 新しいUniqueマクロとIndexマクロにより データの重複を確実に排除し フェッチとクエリの性能を確保できます またそれらの同じプロパティに 削除時に保存した値を追加できます これにより永続的な履歴を利用するときに 削除された旅行を識別できます

    バージョン2から バージョン3に移行するときに さらに旅行を重複排除するために 別のカスタム移行段階が 追加されました iOS 26では サブクラスと 軽量な移行段階を備えた バージョン4が追加されます 現在のバージョンスキーマは バージョン4としてマークされ スキーマ内のすべてのモデルを 新しいサブクラスでリストします サブクラスは iOS 26以降で追加され バージョンスキーマも同様です またこれまでと同じ可用性を持った バージョン3から4への軽量な移行段階を 追加する必要があります 最終的なバージョンスキーマと 移行段階ができたので これらすべてをスキーマ移行計画に カプセル化して バージョンスキーマの順序と 実行する移行段階を 提供できるようにします スキーマ移行計画は リリースされた順に並べられた スキーマの配列で構成されています iOS 26が 利用可能になったら サブクラスによる最新のスキーマと 移行段階の配列を追加し 次のリリースに 移行できるようにします これがスキーマ移行計画の 作成方法となります これでバージョンスキーマと それに対応するスキーマ移行計画を 作成することができました 次に SampleTripsの モデルコンテナを作成するときに それらを利用しましょう modelContainerモディファイアに戻って モデルコンテナを使用するように スキーマ移行計画を使用して モディファイアを変更しましょう まず新しいcontainerプロパティを バージョン4によって バージョン管理されたスキーマを作成する アプリケーションに追加し スキーマ移行計画を ModelContainerイニシャライザに提供します 次に 新しい移行可能な コンテナを使用するように modelContainerモディファイアを変更します これらすべてを行うことで 継承を利用するための SampleTripsのアップデートを クライアントのデータを保持しながら これまでにリリースされた各バージョンから 簡単に移行できるようになります これで移行は処理できたので 次に ビューや移行段階を 生成する際に利用する クエリとフェッチのどこを 改善できるか考えましょう 前回 プレディケートを使用して 選択されたセグメントでクエリを更新しました しかし 過去のビデオでは 検索バーもありました それを元に戻して クライアントが入力した検索テキストを 直接処理しましょう

    最初にsearchTextを指定した プレディケートを構築します まずテキストが空かどうかを確認します 空でない場合は 入力されたテキストに 旅行の名前や目的地が 含まれていないか確認する 複合プレディケートを作成します 次に searchPredicateと classPredicateを使用して 複合プレディケートを作成します 最後に 新しい複合プレディケートを 受け取れるように Queryイニシャライザを変更します この更新によって 検索バーをタップして 入力したテキストで旅行をフィルタリングし セグメントコントロールよりも さらに詳細に絞り込めるようになりました クエリやフェッチをカスタマイズする方法は フィルタリングやソート以外にもあります SwiftDataのフェッチをカスタマイズする その他の方法をいくつか見てみましょう これはバージョン1からバージョン2への カスタム移行段階です willMigrateブロックを使用して すべてのTripをフェッチしています とはいえ 私の重複排除理論では 単一のnameプロパティに アクセスするだけです このプロパティは バージョン2に固有のプロパティだからです これを使用して 他に重複がないことを確認します nameプロパティにしか アクセスしないので fetchDescriptorを変更し propertiesToFetchで nameを指定します これにより Tripモデルは移行時に 必要なデータのみをパッキングできます また 特定の関係が トラバースされることがわかっている場合 例えば この例では重複があったら 宿泊施設を割り当て直しますが その場合 relationshipsToPrefetchを利用して 同じような機能強化を行うことができます livingAccommodation関係を ここに追加しましょう

    これでプリフェッチプロパティを導入できました さらに SampleTripsの 既存のウィジェットコードを変更して 性能を 向上させることができます SampleTripsウィジェットには 最新の旅行を検索できる クエリがあります しかし 単一の値しかフェッチしないため 改善の余地があるかもしれません 現在 ウィジェットコードは フェッチの最初の結果のみを利用しています フェッチ数を制限することで これをもっと効率化できます

    フェッチ数の制限を設定することで プレディケートに一致する 最初の旅行を取得でき 将来 あまりにも多くの休暇が 計画されたときのことは 心配しなくて済むようになります これでクエリとフェッチを改善できました 次に モデルがいつ変更されたかを 知る方法を見てみましょう 永続性モデルはすべてObservableであるため withObservationTrackingを 利用して モデル内の目的のプロパティに行われた変更に 反応させることができます 旅行の開始日と終了日への 変更を監視する場合 datesChangedAlertという 関数を追加することで ユーザーが日付を変更した場合に アラートを表示できます

    同じ方法で永続性モデルに加えられた 多くのローカルの変更を監視できます この方法は ローカルの変更に非常に有用です Observableの最新情報について 詳しくは 「What’s new in Swift」をご確認ください ただし 監視できるのは すべての変更というわけでなく プロセス内でモデルに加えられた変更だけです ウィジェットや拡張機能 またアプリ内の別のモデルコンテナなどの 別のプロセスからデータストアに行われた変更は 監視できません アプリへのローカルまたは内部の変更では 同じモデルコンテナを使用している 複数のモデルコンテキストがある場合があります これらの他のモデルコンテキストは お互いの変更を見ることができます Queryの場合 これらの変更は自動的に適用されます しかし モデルコンテキストの Fetch APIを利用している場合 再フェッチがトリガされるまで 別のモデルコンテキストで行われた変更は 見えません

    さらに 共有App Groupコンテナへの ウィジェットによる保存や 別のアプリによる記述など 外部のアクションによって データが変更されることもあります これらの変更によっても Queryに基づくビューは自動的に更新されます しかし フェッチを使用する場合は 再フェッチする必要があります 再フェッチは 特にモデルの処理に 関係するものが何も変更されていない場合 高くつくことがあります

    幸いなことに SwiftDataには 永続化された履歴があります そのため どのモデルが変更されたか それらがいつ変更されたか 誰がモデルを変更したか どのプロパティが更新されたかまでわかります また preservedValueOnDeletionを Tripの複数プロパティに利用すると 旅行が削除されたときに 履歴にトゥームストーンが残るようになります これを解析することで 削除された旅行を特定できます 履歴について詳しくは WWDC24の「Track Model Changes with SwiftData History」をご覧ください

    では 永続的な履歴を利用して 再フェッチする必要があるかどうか調べましょう 最初に行うことは コンテナから 最後の履歴トークンを フェッチすることです このトークンをデータベースから最後に 読み取った場所を示すマーカとなります これは読み終えたところが わかるように 本に挟んでおく しおりによく似ています HistoryDescriptorを使用して DefaultHistoryTransactionに 履歴フェッチを設定します しかし 永続的な履歴が たくさんある場合 最後のトークンの部分を取得するためだけに 大量のデータをフェッチしなければなりません 幸いなことに iOS 26では 新たにsortByを付けて 履歴をフェッチできるようになりました authorやtransactionIdentifierなど 任意のトランザクションプロパティを 履歴結果をソートするための キーパスとして 指定することができます 新しいsortByを導入して transactionIdentifierに設定し 最新のトランザクションが 最初に来るよう逆順にしておきましょう 最新である 最初のトランザクションだけ 気にすればいいので 結果を1に制限しましょう これで最新の履歴トークンを 効率的にフェッチして 保存できるようになりました このトークンを後で使用できるように保存し そのあとの履歴フェッチで使用しましょう ウィジェットで旅行が更新されるなど 新しい変更が行われると 新しいエントリが履歴に追加されます アプリで履歴をフェッチして 最後のトークン以降に 関係のある変更があるかどうか確認できます これで履歴トークンを保存できたので トークン以降の履歴のみをフェッチする プレディケートを 作成できます 関係のある変更のみを 見つけるために 知りたい変更された エンティティを作成します ここでは 旅行が変更されたかどうか またウィジェットで宿泊先が確定された場合 宿泊施設がどこか知りたいだけです また changesPredicateで エンティティ名を使用して 目的のタイプに 履歴変更を絞り込みます 最後に tokenPredicateと changesPredicateを使用して 複合プレディケートを作成します これらの変更が行われた場合 履歴をフェッチすると トークン以降の 現在のプロセスに関係している エンティティの履歴のみが 取得されます 履歴フェッチの改善により 関係のある変更がない場合に 再フェッチしなくて済むようになります ありがたいことに SwiftDataの履歴では これを簡単に行えます そのおかげで モデルやデータへの ローカルおよびリモートの変更を 監視する方法がわかります ぜひこのビデオを参考にして 永続化が必要な場合に SwiftDataを活用してください モデルグラフを作成する際には 継承が適切かどうか またグラフの発展に伴う 移行の影響を考えましょう データの取得については より機能的で効率的な フェッチやクエリを作成しましょう データの変更時点がわかると 場合により非常に便利です 監視や永続的な履歴についても 取り上げました 私の説明は以上です それではTripをお楽しみください

    • 1:07 - Import SwiftData and add @Model

      // Trip Models decorated with @Model
      import Foundation
      import SwiftData
      
      @Model
      class Trip {
        var name: String
        var destination: String
        var startDate: Date
        var endDate: Date
        
        var bucketList: [BucketListItem] = [BucketListItem]()
        var livingAccommodation: LivingAccommodation?
      }
      
      @Model
      class BucketListItem { ... }
      
      @Model
      class LivingAccommodation { ... }
    • 1:18 - Add modelContainer modifier

      // SampleTrip App using modelContainer Scene modifier
      
      import SwiftUI
      import SwiftData
      
      @main
      struct TripsApp: App {
        var body: some Scene {
          WindowGroup {
            ContentView()
          }
          .modelContainer(for: Trip.self)
        }
      }
    • 1:30 - Adopt @Query

      // Trip App using @Query
      import SwiftUI
      import SwiftData
      
      struct ContentView: View {
        @Query
        var trips: [Trip]
      
        var body: some View {
          NavigationSplitView {
            List(selection: $selection) {
              ForEach(trips) { trip in
                TripListItem(trip: trip)
              }
            }
          }
        }
      }
    • 3:28 - Add subclasses to Trip

      // Trip Model extended with two new subclasses
      
      @Model
      class Trip { 
        var name: String
        var destination: String
        var startDate: Date
        var endDate: Date
        
        var bucketList: [BucketListItem] = [BucketListItem]()
        var livingAccommodation: LivingAccommodation?
      }
      
      @available(iOS 26, *)
      @Model
      class BusinessTrip: Trip {
        var perdiem: Double = 0.0
      }
      
      @available(iOS 26, *)
      @Model
      class PersonalTrip: Trip {
        enum Reason: String, CaseIterable, Codable {
          case family
          case reunion
          case wellness
        }
        
        var reason: Reason
      }
    • 4:03 - Update modelContainer modifier

      // SampleTrip App using modelContainer Scene modifier
      
      import SwiftUI
      import SwiftData
      
      @main
      struct TripsApp: App {
        var body: some Scene {
          WindowGroup {
            ContentView()
          }
          .modelContainer(for: [Trip.self, BusinessTrip.self, PersonalTrip.self])
        }
      }
    • 7:06 - Add segmented control to drive a predicate to filter by Type

      // Trip App add segmented control
      import SwiftUI
      import SwiftData
      
      struct ContentView: View {
        @Query
        var trips: [Trip]
        
        enum Segment: String, CaseIterable {
          case all = "All"
          case personal = "Personal"
          case business = "Business"
        }
        
        init() {
          let classPredicate: Predicate<Trip>? = {
            switch segment.wrappedValue {
            case .personal:
              return #Predicate { $0 is PersonalTrip }
            case .business:
              return #Predicate { $0 is BusinessTrip }
            default:
              return nil
            }
          }
          _trips = Query(filter: classPredicate, sort: \.startDate, order: .forward)
        }
        
        var body: some View { ... }
      }
    • 8:26 - SampleTrips Versioned Schema 2.0

      enum SampleTripsSchemaV2: VersionedSchema {
        static var versionIdentifier: Schema.Version { Schema.Version(2, 0, 0) }
        static var models: [any PersistentModel.Type] {
          [SampleTripsSchemaV2.Trip.self, BucketListItem.self, LivingAccommodation.self]
        }
      
        @Model
        class Trip {
          @Attribute(.unique) var name: String
          var destination: String
      
          @Attribute(originalName: "start_date") var startDate: Date
          @Attribute(originalName: "end_date") var endDate: Date
          
          var bucketList: [BucketListItem]? = []
          var livingAccommodation: LivingAccommodation?
          
          ...
        }
      }
    • 8:41 - SampleTrips Custom Migration Stage from Version 1.0 to 2.0

      static let migrateV1toV2 = MigrationStage.custom(
         fromVersion: SampleTripsSchemaV1.self,
         toVersion: SampleTripsSchemaV2.self,
         willMigrate: { context in
            let fetchDesc =  FetchDescriptor<SampleTripsSchemaV1.Trip>()
            let trips = try? context.fetch(fetchDesc)
        
            // De-duplicate Trip instances here...
      
            try? context.save()
          }, 
          didMigrate: nil
      )
    • 9:09 - SampleTrips Versioned Schema 3.0

      enum SampleTripsSchemaV3: VersionedSchema {
        static var versionIdentifier: Schema.Version { Schema.Version(3, 0, 0) }
        static var models: [any PersistentModel.Type] {
          [SampleTripsSchemaV3.Trip.self, BucketListItem.self, LivingAccommodation.self]
        }
      
        @Model
        class Trip {
          #Unique<Trip>([\.name, \.startDate, \.endDate])
          #Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate])
      
          @Attribute(.preserveValueOnDeletion)
          var name: String
          
          @Attribute(hashModifier:@"v3")
          var destination: String
      
          @Attribute(.preserveValueOnDeletion, originalName: "start_date")
          var startDate: Date
      
          @Attribute(.preserveValueOnDeletion, originalName: "end_date")
          var endDate: Date
        }
      }
    • 9:33 - SampleTrips Custom Migration Stage from Version 2.0 to 3.0

      static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SampleTripsSchemaV2.self,
        toVersion: SampleTripsSchemaV3.self,
        willMigrate: { context in
          let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV2.Trip>())
      
          // De-duplicate Trip instances here...
      
          try? context.save()
        }, 
        didMigrate: nil
      )
    • 9:50 - SampleTrips Versioned Schema 4.0

      @available(iOS 26, *)
      enum SampleTripsSchemaV4: VersionedSchema {
        static var versionIdentifier: Schema.Version { Schema.Version(4, 0, 0) }
      
        static var models: [any PersistentModel.Type] {
          [Trip.self, 
           BusinessTrip.self, 
           PersonalTrip.self, 
           BucketListItem.self,
           LivingAccommodation.self]
        }
      }
    • 10:03 - SampleTrips Lightweight Migration Stage from Version 3.0 to 4.0

      @available(iOS 26, *)
      static let migrateV3toV4 = MigrationStage.lightweight(
        fromVersion: SampleTripsSchemaV3.self,
        toVersion: SampleTripsSchemaV4.self
      )
    • 10:24 - SampleTrips Schema Migration Plan

      enum SampleTripsMigrationPlan: SchemaMigrationPlan {
        static var schemas: [any VersionedSchema.Type] {
          var currentSchemas: [any VersionedSchema.Type] =
            [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
          if #available(iOS 26, *) {
            currentSchemas.append(SampleTripsSchemaV4.self)
          }
          return currentSchemas
        }
      
        static var stages: [MigrationStage] {
          var currentStages = [migrateV1toV2, migrateV2toV3]
          if #available(iOS 26, *) {
            currentStages.append(migrateV3toV4)
          }
          return currentStages
        }
      }
    • 10:51 - Use Schema Migration Plan with ModelContainer

      // SampleTrip App update modelContainer Scene modifier for migrated container
      
      @main
      struct TripsApp: App {
      
        let container: ModelContainer = {
          do {
            let schema = Schema(versionedSchema: SampleTripsSchemaV4.self)
            container = try ModelContainer(
              for: schema, migrationPlan: SampleTripsMigrationPlan.self)
          } catch { ... }
          return container
        }()
        var body: some Scene {
          WindowGroup {
            ContentView()
          }
          .modelContainer(container)
        }
      }
    • 11:48 - Add search predicate to Query

      // Trip App add search text to predicate
      struct ContentView: View {
        @Query
        var trips: [Trip]
      
        init( ... ) {
          let classPredicate: Predicate<Trip>? = {
            switch segment.wrappedValue {
            case .personal:
              return #Predicate { $0 is PersonalTrip }
            case .business:
              return #Predicate { $0 is BusinessTrip }
            default:
              return nil
            }
          }
          
          let searchPredicate = #Predicate<Trip> {
            searchText.isEmpty ? true : 
              $0.name.localizedStandardContains(searchText) ||              
              $0.destination.localizedStandardContains(searchText)
          }
          
          let fullPredicate: Predicate<Trip>
          if let classPredicate {
            fullPredicate = #Predicate { classPredicate.evaluate($0) &&
                                         searchPredicate.evaluate($0)}
          } else { 
            fullPredicate = searchPredicate
          }
          _trips = Query(filter: fullPredicate, sort: \.startDate, order: .forward)
        }
        var body: some View { ... }
      }
    • 12:31 - Tailor SwiftData Fetch in Custom Migration Stage

      static let migrateV1toV2 = MigrationStage.custom(
         fromVersion: SampleTripsSchemaV1.self,
         toVersion: SampleTripsSchemaV2.self,
         willMigrate: { context in
            var fetchDesc =  FetchDescriptor<SampleTripsSchemaV1.Trip>()
            fetchDesc.propertiesToFetch = [\.name]
      
            let trips = try? context.fetch(fetchDesc)
        
            // De-duplicate Trip instances here...
      
            try? context.save()
          }, 
          didMigrate: nil
      )
    • 13:11 - Add relationshipsToPrefetch in Custom Migration Stage

      static let migrateV1toV2 = MigrationStage.custom(
         fromVersion: SampleTripsSchemaV1.self,
         toVersion: SampleTripsSchemaV2.self,
         willMigrate: { context in
            var fetchDesc =  FetchDescriptor<SampleTripsSchemaV1.Trip>()
            fetchDesc.propertiesToFetch = [\.name]
            fetchDesc.relationshipKeyPathsForPrefetching = [\.livingAccommodation]
      
            let trips = try? context.fetch(fetchDesc)
        
            // De-duplicate Trip instances here...
      
            try? context.save()
          }, 
          didMigrate: nil
      )
    • 13:28 - Update Widget to harness fetchLimit

      // Widget code to get new Timeline Entry
      
      func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let currentDate = Date.now
        var fetchDesc = FetchDescriptor(sortBy: [SortDescriptor(\Trip.startDate, order: .forward)])
        fetchDesc.predicate = #Predicate { $0.endDate >= currentDate }
      
        fetchDesc.fetchLimit = 1
        
        let modelContext = ModelContext(DataModel.shared.modelContainer)
        if let upcomingTrips = try? modelContext.fetch(fetchDesc) {
          if let trip = upcomingTrips.first { ... }
          
        }
      }
    • 16:24 - Fetch the last transaction efficiently

      // Fetch history with sortBy and fetchlimit to get the last token
      
      var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>()
      historyDesc.sortBy = [.init(\.transactionIdentifier, order: .reverse)]
      historyDesc.fetchLimit = 1
      
      let transactions = try context.fetchHistory(historyDesc)
      if let transaction = transactions.last {
        historyToken = transaction.token
      }
    • 17:29 - Fetch History after the given token and only for the entities of concern

      // Changes AFTER the last known token
      let tokenPredicate = #Predicate<DefaultHistoryTransaction> { $0.token > historyToken }
      
      // Changes for ONLY entities of concern
      let entityNames = [LivingAccommodation.self, Trip.self]
      let changesPredicate = #Predicate<DefaultHistoryTransaction> {
                               $0.changes.contains { change in
                                 entityNames.contains(change.changedPersistentIdentifier.entityName)
                               }
                             }
      
      
      let fullPredicate = #Predicate<DefaultHistoryTransaction> {
                            tokenPredicate.evaluate($0)
                            &&
                            changesPredicate.evaluate($0)
                          }
      
      let historyDesc = HistoryDescriptor<DefaultHistoryTransaction>(predicate: fullPredicate)
      let transactions = try context.fetchHistory(historyDesc)

Developer Footer

  • ビデオ
  • WWDC25
  • SwiftData:継承とスキーマの移行の詳細
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習とAI
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン