SwiftDataの真のコストはスキーマの規律にある
Get BananasのShoppingItemは、SwiftDataにおけるスキーマの規律がなぜ重要なのかを示す代表的な例です。当初のスキーマにはlastModifiedのタイムスタンプが含まれていませんでした。後からこれを追加するには特定のマイグレーションの形が必要となりました。既存のデータがすでにディスク上に存在していたためです。そしてこのフィールドは、最初に非オプショナルとして追加した際に発生したマイグレーションのクラッシュを修正するために、あえてオプショナルにされました。1
SwiftDataのAPIは2つのマクロだけです。クラスに付けた@Modelによって、それは永続化される型になります。プロパティに付けた@Attribute(.unique)によって、一意性の制約が与えられます。このフレームワークは、Core Dataのスタック管理、value-transformerの煩雑な処理、そしてNSManagedObjectContextのボイラープレートを隠してくれます。一方で、フレームワークが隠してくれないものがスキーマのマイグレーションです。命令的ではなく宣言的にするだけなのです。マイグレーションに注意を払わなかったときのコストは、何気ないアップデートでユーザーのデータを消し去ってしまうバグです。
主張はこうです。SwiftDataは始めるのは安価でも、ずさんにマイグレーションすると高くつきます。規律とは、命名、オプショナル性、そしてVersionedSchemaを初日から取り入れることであり、必要だったと気づいた日からではありません。
TL;DR
@Modelマクロは、クラスを永続化されるSwiftDataの型へと変えます。フレームワークはコンパイル時にプロパティ宣言からスキーマを生成します。- 新しいオプショナルなプロパティの追加は何もしないマイグレーションです。SwiftDataの軽量マイグレーションがこれを処理します。既存のスキーマに非オプショナルなプロパティを追加するには、
VersionedSchemaに加えて、既存の行に対して新しいフィールドをどう埋めるかをフレームワークに伝えるMigrationPlanが必要です。 VersionedSchemaを初日から導入しないことのコストは、些細でないv2のスキーマ変更がユーザーのデータベースを破棄しかねないことです。軽量パスは保守的であり、マイグレーションを推論できない場合は処理を打ち切るためです。@Attribute(.unique)は自然キー(自分で生成したUUIDや、インポートした外部ID)に適したツールです。@Relationshipは親子関係の参照に適したツールです。どちらもマクロであり、内部で適切なCore Dataの配管を生成します。2
@Modelが実際に行っていること
SwiftDataの型は、@Modelマクロを適用したSwiftのクラスです。Get BananasのShoppingItemが代表的な形です。
import Foundation
import SwiftData
@Model
final class ShoppingItem {
@Attribute(.unique) var id: UUID
var name: String
var amount: String
var section: String
var isChecked: Bool
var isOptional: Bool
var sortOrder: Int
var lastModified: Date?
init(id: UUID = UUID(), name: String, amount: String, section: String,
isOptional: Bool = false, sortOrder: Int = 0) {
self.id = id
self.name = name
self.amount = amount
self.section = section
self.isChecked = false
self.isOptional = isOptional
self.sortOrder = sortOrder
self.lastModified = Date()
}
}
この形についてAPIが隠している3つの詳細があります。
@Modelは別途の永続ストアのスキーマ宣言を必要としません。 SwiftDataはコンパイル時にクラス定義を読み取り、スキーマを合成します。クラスのプロパティがモデルの属性になり、それらのSwiftの型がカラムの型になります。維持すべき.xcdatamodeldファイルは存在しません(ただし、Core Dataの基盤となるNSManagedObjectModelは依然として存在し、実行時にスキーマを支えているのはそれです)。2
@Attribute(.unique)は単一カラムに対する制約であり、PRIMARY KEYの宣言ではありません。 SwiftDataの永続的なアイデンティティはPersistentIdentifierであり、これは行ごとに自動生成されます。@Attribute(.unique)の宣言は、フレームワークに「このカラムは1つの値につき最大1行しか格納しない」と伝えます。すでに存在する.unique値を持つモデルを挿入すると、SwiftDataはupsertを実行します。既存の行が拒否されるのではなく更新されるのです。このセマンティクスはプロダクトコードにおいて重要です。.uniqueは重複の送信を防ぐUIレベルのバリデーションではなく、静かにマージを行う「最大1つ」のストレージ保証です。上記のid: UUIDパターンはプロセス間同期に推奨される形であり(プロセス内のPersistentIdentifierが失われても残り続ける安定した識別子が欲しい場合)、このupsertの挙動は、同じUUIDが2つの同期経路から届いたときにまさに望ましい動作です。
@Modelクラスは値型ではなく参照型です。 ShoppingItemインスタンスのプロパティを変更すると、SwiftDataの変更追跡が起動します。フレームワークがその変更を登録し、次のコンテキスト保存時に永続化します。@Queryを通じたSwiftUI統合は、一致するpredicateを監視しているビューを再描画します。このパターンは(What SwiftUI Is Made Ofで扱った)@Observableに似ており、その上に永続化が重ねられています。
オプショナルなフィールドは安価なマイグレーション
ShoppingItemのlastModified: Date?フィールドはオプショナルであり、このオプショナル性が要となっています。このフィールドはv1のリリース後、デバイス間同期と競合解決をサポートするために追加されました。ユーザーのデバイス上の既存の行にはlastModifiedの値がありませんでした。デフォルト値のないオプショナルなフィールドであれば、SwiftDataの軽量マイグレーションがマイグレーションコードを一切書かずに追加を処理できます。既存の行にはnilが入り、新しい行にはinitが設定した値が入ります。3
軽量マイグレーションのパスは、フレームワークの礼儀正しい経路です。SwiftDataは新しいスキーマと永続ストアを調べ、互換性のある最小の変更を推論し、それを適用します。マイグレーションは自動で行われ、ユーザーには何も見えず、アプリは既存のデータ上で通常どおり起動します。軽量パスがきれいに処理できるケースは次のとおりです。
- オプショナルなプロパティの追加
- プロパティの削除(データは破棄され、既存の読み取りはそのカラムを見なくなります)
- フレームワークがヒントによって対応付けできる属性のリネーム(
@Attribute(originalName: ...)の使用) - フレームワークが対応付けできる
@Modelクラスのリネーム(@Model.originalNameまたはヒントの使用)
軽量パスが処理を打ち切るケースは次のとおりです。
- デフォルト値のない非オプショナルなプロパティを既存のスキーマに追加する場合(既存の行にはそれを埋める値がありません)
- プロパティの型の変更(例:
Int→String) - 1つのモデルを2つに分割する、あるいは2つを1つに統合する
- マイグレーションにカスタムロジックを必要とするもの全般
軽量パスが処理を打ち切るとき、安全な挙動はマイグレーションを失敗させることです。安全でない挙動はデータベースを破棄して最初からやり直すことでしょう。フレームワークは保守的であり、それを黙って行うことを拒否します。ユーザーは起動時にマイグレーションエラーでアプリがクラッシュするのを目にし、開発者はスキーマの不一致を指すスタックトレースを目にします。誰もデータを失いませんが、誰もが信頼を失います。
VersionedSchemaを初日から導入しないことのコストは、v2からv3への境界で表面化します。軽量パスが処理できる範囲を超えるスキーマ変更を伴う3つ目の機能を追加したときです。
VersionedSchemaとMigrationPlan:初日からの規律
VersionedSchemaはモデルスキーマの特定のバージョンを宣言します。MigrationPlanはあるバージョンから次のバージョンへどうマイグレーションするかを宣言します。4 その形は次のとおりです。
import SwiftData
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV1.self]
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV2.self]
}
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [
SchemaV1.self,
SchemaV2.self,
]
static var stages: [MigrationStage] = [
MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
]
}
モデルクラス自体はversioned-schemaの名前空間へと移ります。
extension SchemaV1 {
@Model
final class ShoppingItemV1 { /* v1 fields */ }
}
extension SchemaV2 {
@Model
final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}
ModelContainerはマイグレーションプランとともに構築します。
let container = try ModelContainer(
for: ShoppingItemV2.self,
migrationPlan: AppMigrationPlan.self,
configurations: ModelConfiguration("ShoppingList")
)
マイグレーションプランは、スキーマがどう進化するかについての型付きのグラフをフレームワークに与えます。v2をリリースしたアプリがv1のデータベースに対して起動すると、フレームワークはマイグレーションプランをたどり、名前付きのステージを適用し、データベースをv2へと引き上げます。v3をリリースする際には、schemasにSchemaV3.selfを追加し、v2とv3の間に新しいMigrationStageを追加します。
規律とは、バージョンが1つしかなくても、v1でVersionedSchemaをリリースすることです。そうするコストは、1つの追加ファイルと1つの追加のenum宣言です。そうしないコストは、v2の最初の些細でないスキーマ変更の際に、v1を遡ってVersionedSchemaでラップしなければならないことです。これは可能ではありますが、フレームワークが既存のデータをSchemaV1として識別できるよう、v1の正確な形に合わせる慎重さが必要です。v2に取り組む未来のあなたがその税を払うことになります。今のあなたなら、一度払って忘れてしまえるのです。
難しいケースのためのカスタムMigrationStage
軽量マイグレーションはほとんどの追加的な変更をカバーします。型の変更、分割、統合、そして条件付きの値埋めにはMigrationStage.customが必要です。
static var stages: [MigrationStage] = [
MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// Read v1 rows; stage any derived state to a transient store
// (UserDefaults / temp file) since the v1 and v2 contexts do
// not share state, and didMigrate cannot read v1.
let v1Items = try context.fetch(FetchDescriptor<ShoppingItemV1>())
stageDerivedState(from: v1Items)
},
didMigrate: { context in
// Populate v2-only fields on existing rows
let v2Items = try context.fetch(FetchDescriptor<ShoppingItemV2>())
for item in v2Items where item.lastModified == nil {
item.lastModified = Date()
}
try context.save()
}
)
]
2つのクロージャは、フレームワークが構造的マイグレーションを適用する前と後に発火します。willMigrateはv1スキーマに対して実行され、didMigrateはv2スキーマに対して実行されます。クロージャの本体は通常のSwiftDataコード(fetch descriptor、モデルコンテキストの保存、実行中のアプリで使うのと同じAPI)であり、マイグレーション中の一時的なコンテキストに対して動作します。
本番で生き残るパターンは、willMigrateを空のままにし、すべての値埋めロジックをdidMigrateに置くことです。willMigrate内でv1データを読むことは許されていますが、フレームワークの視点ではv2スキーマがまだ存在しないため、あらゆる計算はdidMigrateクロージャが読める一時的なストアにステージングしなければなりません。よりシンプルなルールはこうです。構造的マイグレーションはフレームワークの仕事であり、既存の行にv2固有のフィールドを埋めるのはdidMigrateの仕事です。
@Attributeと@Relationshipがその名にふさわしいとき
@Modelクラスにおけるスキーマの装飾作業のほとんどは、2つのマクロが担います。
@Attributeは単一のプロパティを制約やヒントで装飾します。
@Attribute(.unique)は、ShoppingItem.idのように一意性を強制します@Attribute(.externalStorage)は、大きなDataのblob(画像データ、音声バッファ)をデータベースの外に格納します@Attribute(originalName: "old_field_name")は、マイグレーション中にプロパティをリネームされたカラムに対応付けます@Attribute(.transformable(by: ...))は、Codableでない型にValueTransformerを適用します
適切な規律はこうです。本当に一意であるべきフィールド(自分で生成したUUID、外部ID)には.uniqueを使い、数KBを超えるblobには.externalStorageを使い、プロパティのv2でのリネームが本来ならv1のデータを失わせてしまう場合にはoriginalNameを使いましょう。
@Relationshipは、別の@Modelクラス、またはそのコレクションを指すプロパティを装飾します。
@Model
final class List {
var name: String
@Relationship(deleteRule: .cascade, inverse: \ShoppingItem.list)
var items: [ShoppingItem] = []
}
@Model
final class ShoppingItem {
var name: String
var list: List?
}
deleteRule: .cascadeは、親のListを削除すると、子のShoppingItem行がすべて削除されることを意味します。inverse:パラメータは、子のどのプロパティが親を指し返しているかをフレームワークに伝えます。フレームワークはこれを、予測可能な双方向の整合性維持に使います。SwiftDataは逆方向を自動的に推論できる場合もあり、明示的に単方向の関係にはinverse: nilがサポートされています。とはいえ、推論が曖昧になりうる場合は常にinverse:を宣言するのが安全なデフォルトです。5
適切な規律はこうです。関係は明示的なdeleteRuleとともに宣言し(デフォルトは.nullifyですが、これが望ましいことはまれです)、関係が双方向であれば(フレームワークの推論に頼るのではなく)常にinverse:を宣言しましょう。暗黙のデフォルトはたいてい間違っています。明示的な形は追加のパラメータ1つと、永久に保存されるバグとの引き換えなのです。
アクター境界を越える:グラフではなく識別子を送る
@ModelクラスはSendableではなく、正しい一手はそれをSendableにしようとするのをやめることです。インスタンスはModelContextが保持する生きたオブジェクトグラフへの参照です。フレームワークはそのグラフが別のアクターから読み取って安全だと保証できないため、この型は意図的に非Sendableのままにされています。準拠を強制してもデータ競合がなくなるわけではなく、隠れるだけです。7
うまくいくパターンは、アイデンティティとプレーンな値を送り、反対側で再フェッチすることです。PersistentIdentifierはSendableなので、境界をきれいに越えます。宛先が必要とするスカラー値(名前、フラグ、小さな構造体に入れた差分)を取り出し、それらを識別子とともに渡し、受け取り側のアクターが識別子を使って自身のコンテキストからモデルを再フェッチするようにします。
// On the source actor: extract identity + plain values, never the model.
let id: PersistentIdentifier = item.persistentModelID
let snapshot = ItemSnapshot(name: item.name, isChecked: item.isChecked)
// On the destination actor: re-fetch from this context, then mutate.
let fetched = destinationContext.model(for: id) as? ShoppingItem
避けるべき失敗のかたちは、モデルグラフそのものを渡すことです。グラフの一部が境界を越えると、受け取り側は反対側で部分的にしかハイドレートされないモデルを手にします。元のコンテキストでフォルトインされていなかった関係や遅延読み込みされるプロパティが、間違ったコンテキストに対して解決される(あるいはまったく解決されない)のです。そして続いて起こるバグは静かな類のものです。識別子と取り出した値は安全な契約ですが、グラフはそうではありません。ModelActorは、コンテキストを所有し、インスタンスではなく値を提供することで、この規律を体現します。7
CloudKit同期とapp-groupエンタイトルメントの罠
widgetやextensionがSwiftDataのストアを読めるよう、それをapp-groupコンテナへ移動すると、リリース後のアプリを噛むかたちでCloudKit同期と相互作用します。2つの事実から残りが導かれます。
第一に、ストアの場所です。デフォルトのModelConfigurationでは、アプリがグループなしからapp-groupへ進化する際、SwiftDataが既存のストアをapp-groupコンテナへコピーしてくれます。Appleの表現では、SwiftDataは「既存のストアをapp groupコンテナへコピーする」とされています。8 カスタムのストアURLを使う場合は、場所を自分で所有します。自分でファイルを新しいコンテナへコピーし、設定をそこへ向けるのです。デフォルトの経路が便利なのは、まさにフレームワークがコピーを行ってくれるからです。カスタムの経路は、その利便性と引き換えに制御を得ます。
第二に、エンタイトルメントです。CloudKitで同期されるストアを読むapp-groupのメンバーは、すべて同じCloudKitエンタイトルメントを持たなければなりません。それらの各プロセスが、自身のためにそのコンテナを同期するからです。この要件こそが罠です。widgetやextensionには冗長な同期を駆動する実行時の予算もフォアグラウンドのウィンドウもなく、CloudKitエンタイトルメントを渡せばそれを試みざるを得なくなります。修正策は、2つのModelConfigurationインスタンスに分けることです。1つは同期されるストア(CloudKitエンタイトルメントを持ち、メインアプリが所有する)、もう1つはapp-groupコンテナ内のローカルなストアで、widgetやextensionが同期を一切行わずに読むものです。フォアグラウンドのアプリがうまく行える場所に同期を置き、読み取り目的で共有するデータは同期経路の外に保ちましょう。8
私なら違うかたちで作るもの
このクラスタのアプリが実際にリリースしている、あるいはリリースしておけばよかったと思う3つのパターンです。
VersionedSchemaをv1からリリースする。 リリースされるすべての@Modelクラスは、初日からVersionedSchemaの内側に置くべきです。コストはスキーマバージョンごとに1つのラップするenumです。利点は、v2の最初の些細でない変更が、2日がかりの遡及的なリファクタリングではなく、MigrationPlan.schemasへの1行追加で済むことです。
すべてのタイムスタンプをオプショナルにする。 デバイス間同期や競合解決のために存在するlastModified、createdAt、updatedAtのようなフィールドは、v1のプロダクトがそれらを必要としないなら、v1ではオプショナルにすべきです。オプショナル性は、(実際に必要になる)v2へのマイグレーションを安価に保ちます。didMigrate中に既存の行を埋めるのはループ1つで済みますが、v1から非オプショナルにするのは、ユーザーデータのバックフィルを壊しうる制約です。
PersistentIdentifierではなく、UUIDを自然キーとして使う。 SwiftDataのPersistentIdentifierはプロセス内のものです。デバイス間同期、MCP統合(Two Agent Ecosystems, One Shopping Listで扱いました)、そしてあらゆるプロセス外の参照には、安定した識別子が必要です。@Attribute(.unique)を付けたUUIDが正しい形です。プロセス境界を越えるものには、プロセス内のPersistentIdentifierは間違った形です。
@Modelが間違った答えになるとき
SwiftDataが適切なツールではない3つのケースです。
単一レコードのキー/バリュー状態。 アプリの設定、ユーザーが選択した言語、最後の同期のタイムスタンプ。UserDefaultsまたはNSUbiquitousKeyValueStoreを使いましょう(Five Apple Platforms, Three Shared Filesで扱いました)。1行のためのSwiftDataのオーバーヘッドは無駄な儀式です。キー/バリューストアが正しい基盤です。
オフライン書き込みのないサーバー権威のデータ。 REST APIから取得して読み取り専用で表示するリスト。信頼の源がサーバーであり、ローカルキャッシュが単なるキャッシュであるなら、SwiftDataは過剰です。Documents/内のシンプルなCodableスナップショットと、メモリにキャッシュした配列で十分です。データがハードリセットを生き延びる必要がないなら、SwiftDataのマイグレーション税は払う価値がありません。
複数プロセス間の協調。 SwiftDataはプロセス内で動作します。iOSアプリの外で動くMCPサーバーは、アプリのSwiftDataコンテナを読み書きできません。プロセスをまたぐ状態には別の形が必要です。iCloud DriveのJSONファイル、共有されたApp Groupコンテナ、あるいはプロセス間を橋渡しする明示的な同期レイヤーです。(Get Bananasがまさにこの理由でSwiftDataとiCloud DriveのJSONを組み合わせています。)6
データがめったに変わらない大きなblobである。 10MBの音声ファイル、50MBの画像データセット。blobがSwiftDataの行の内側にあるなら@Attribute(.externalStorage)を使い、そうでなければファイルシステムを直接使い、ファイルURLを指すメタデータをSwiftDataに置きましょう。
このパターンがiOS 26+でリリースするアプリにとって意味すること
3つの要点です。
-
マクロは簡単な部分です。マイグレーションこそがコストです。
@Modelと@Attributeは、多くのCore Dataの配管を隠す2行の宣言です。マイグレーションの規律こそが、アプリのライフタイムを通じて実際に支払うものです。v2を念頭にv1を設計しましょう。 -
VersionedSchemaを初日から導入することは、リリースするアプリにとって譲れません。 ラップするenumは1つの追加ファイルです。後から追加する遡及的なコストははるかに高くつきます。 -
オプショナルなフィールドと明示的な関係は、安価な保険です。 同期メタデータ用のオプショナルなタイムスタンプ、関係に対する明示的な
deleteRuleとinverse:。どちらもごく小さな宣言でありながら、v2の柔軟性を大きく買ってくれます。
Apple Ecosystemクラスタの全体像はこうです。Apple Intelligence向けの型付きApp Intents、LLMをまたぐエージェント向けのMCPサーバー、両者の間のルーティングの問い、オンデバイスLLMとToolプロトコルのためのFoundation Models、iOSのロック画面の状態機械のためのLive Activities、Apple Watch上のwatchOSランタイムの契約、フレームワークの基盤としてのSwiftUI内部、visionOSのシーンのためのRealityKitの空間的なメンタルモデル、ビジュアルレイヤーのためのLiquid Glassのパターン、デバイス間のリーチのためのマルチプラットフォームのリリース。ハブはApple Ecosystem Seriesにあります。AIエージェントとiOSに関するより広い文脈については、iOS Agent Development guideをご覧ください。
FAQ
@ModelとCore DataのNSManagedObjectの違いは何ですか?
@Modelは、内部でNSManagedObjectの配管を生成するSwiftのマクロです。SwiftDataはCore Dataを背後のストアとして使うため、実行時のモデルは同じです。違いは表面にあります。@Modelは.xcdatamodeldファイル、value-transformerの儀式、そしてNSManagedObjectContextのライフサイクル管理を取り除きます。同じ永続ストアを、Swiftらしい形のAPIで得られるのです。
スキーマを変更するつもりがまったくない場合でもVersionedSchemaは必要ですか?
アプリがv2をリリースする可能性があるなら、はい。一度きりのデモなら、いいえ。VersionedSchemaをv1から導入するコストは、1つの追加のenum宣言です。v2で遡って追加するコストは、フレームワークが既存のデータを認識できるようv1の正確なスキーマの形に合わせることであり、これは可能ではあってもエラーを招きやすいものです。ほとんどのリリースされるアプリはいずれスキーマ変更を必要とします。v1のうちにその予算を見込んでおきましょう。
@Attribute(.unique)はいつ使うべきですか?
そのフィールドが行の自然キーであるとき、つまり自分で生成したUUID、インポートした外部ID、割り当てたslugであるときです。SwiftDataは.uniqueをupsertとして扱います。すでに存在する.unique値を持つモデルを挿入すると、新しい行が追加されるのではなく既存の行が更新されます。このセマンティクスこそが、upsert形式の同期経路(同じUUIDが2つのデバイスから届く)を安全にしています。そしてそれは同時に、titleのような表示名のフィールドに.uniqueが間違ったツールである理由でもあります。2人のユーザーが同じタイトルを入力すると、2つの異なるレコードが生まれる代わりに、静かに行がマージされてしまうからです。
既存のスキーマに追加された非オプショナルなフィールドはどう扱えばよいですか?
既存の行にそのフィールドを埋めるdidMigrateクロージャを持つMigrationStage.customを使いましょう。あるいは、より簡単には、新しいスキーマバージョンでそのフィールドをオプショナルとして宣言し、アクセス時に遅延的に埋めましょう。オプショナル性はより安価なマイグレーションです。非オプショナルな追加には明示的な値埋めロジックが必要です。
PersistentIdentifierと自分のUUIDの違いは何ですか?
PersistentIdentifierはSwiftDataのプロセス内の行IDです。自動生成され、実行中のプロセスのライフタイムを通じて存続します。@Attribute(.unique)を付けた自分のUUIDは、プロセスをまたぎ、デバイスをまたぐ安定した識別子です。アプリ内のプロセス内参照にはPersistentIdentifierを使いましょう。プロセス境界を越えるもの(デバイス間同期、外部統合、MCPツール、ネットワーク呼び出し)にはUUIDを使いましょう。
References
-
著者のGet Bananas。SwiftDataをiCloud DriveのJSON同期とMCPサーバーと組み合わせたSwiftUIの買い物リストアプリです。
ShoppingItemモデルは開発初期のサイクルを通じて進化しました。lastModified: Date?フィールドは当初のスキーマの後に追加されました(2025-12-01のコミット268a00d、”Make lastModified optional to fix migration crash”)。非オプショナルにすると、既存の行に埋める値がない場合にマイグレーションが壊れたためです。 ↩ -
Apple Developer、“SwiftData”および“Adding and editing persistent data in your app”。
@Modelマクロ、@Attribute制約の表面、そしてCore DataのNSManagedObjectModelとの関係について。 ↩↩ -
Apple Developer、“Preserving your app’s model data across launches”および“Adopting SwiftData for a Core Data app”。軽量マイグレーションのセマンティクスと、フレームワークが処理を打ち切る要因について。 ↩
-
Apple Developer、“VersionedSchema”および“SchemaMigrationPlan”。バージョン付きスキーマの宣言、マイグレーションステージの定義、そしてマイグレーションプランを受け取る
ModelContainerのコンストラクタについて。 ↩ -
Apple Developer、“Defining data relationships with enumerations and model classes”および“Schema.Relationship”。
@Relationshipマクロ、deleteRuleのオプション(.cascade、.nullify、.deny、.noAction)、そして双方向の関係維持におけるinverse:パラメータの役割について。 ↩ -
著者の分析、Two Agent Ecosystems, One Shopping List、2026年4月29日、およびFive Apple Platforms, Three Shared Files。Get Bananas + Returnの、複数プロセスにまたがるワークフローの内側でSwiftDataを補完する(時には置き換える)プロセス間およびデバイス間の同期パターンについて。 ↩
-
Apple Developer、“PersistentIdentifier”(
Sendableに準拠)および“ModelActor”。SwiftDataチームはWWDC 2026のSwiftData Group Labで、@ModelオブジェクトはSendableではなく、準拠を強制すべきではないと確認しました。それらはコンテキストの内側に存在する参照グラフだからです。推奨される境界の契約は、SendableなPersistentIdentifierと取り出したプレーンな値を渡し、宛先のコンテキストで再フェッチすることであり、モデルグラフを渡すと受け取り側に部分的にしかハイドレートされないオブジェクトが残るとされています。WWDC 2026のSwiftData Group Labをローカルで文字起こしした録音からの言い換えです。Appleはラボの公式キャプションを公開していません。 ↩↩ -
Apple Developer、“Adopting SwiftData for a Core Data app”。デフォルトの設定では「SwiftDataが既存のストアをapp groupコンテナへコピーする」一方、カスタムのストアURLでは場所の管理が自分に委ねられると述べられています。CloudKitで同期されるストアを読むapp-groupメンバーに対するCloudKitエンタイトルメントの要件と、widgetやextensionを同期経路の外に保つための2つの
ModelConfigurationへの分割(1つは同期、1つはローカル)は、WWDC 2026のSwiftData Group Labで説明されました。WWDC 2026のSwiftData Group Labをローカルで文字起こしした録音からの言い換えです。Appleはラボの公式キャプションを公開していません。 ↩↩