View in English

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

クイックリンク

5 クイックリンク

ビデオ

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

WWDC24に戻る

ストリーミングはほとんどのブラウザと
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

    リソース

    • Forum: Programming Languages
    • SwiftData
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC24

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

    皆さん こんにちは 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)
          }
      }

Developer Footer

  • ビデオ
  • WWDC24
  • 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
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習
    • オープンソース(英語)
    • セキュリティ
    • 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.
    利用規約 プライバシーポリシー 契約とガイドライン