swiftdata-coredata-persistence について
swiftdata-coredata-persistenceは、SwiftDataとCoreDataの選択や運用を支援するiOS開発者向けのリファレンススキルです。フレームワークの選定、移行時のリスク、iCloudのスキーマ制約、データ取得と保存のパターンを扱います。
主な機能
- 01SwiftDataとCoreDataの適用場面を比較
- 02バージョン管理と複雑な移行のリスクを説明
- 03iCloud同期時のスキーマ制約を整理
- 04バッチ取得とリレーション読み込みのパターン
- 05大容量データのファイル保存を推奨
- 06移行チェックリストとよくある問題を掲載
活用シーン
- 01新しいiOSアプリの永続化方式を選ぶ
- 02iCloud同期を伴うスキーマ移行を計画
- 03起動遅延や移行失敗を調査
- 04大量レコードやメディアの取得を改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dagba/ ios-mcp --skill swiftdata-coredata-persistenceインストール後、Claude Code で/swiftdata-coredata-persistenceを入力して読み込まれたか確認します。
詳細
SwiftData と CoreData による永続化
概要
SwiftData と CoreData は互換性がありません。 それぞれに固有の強み、移行時の落とし穴、パフォーマンスのパターンがあります。フレームワークや移行戦略を誤って選ぶと、本番環境でクラッシュが発生します。
基本原則: アプリの制約に基づいてフレームワークを選び、初日からモデルをバージョン管理し、データベースに BLOB を保存しないでください。
フレームワークの選択
digraph framework_choice {
"New iOS app?" [shape=diamond];
"iOS 17+ only?" [shape=diamond];
"Simple data model?" [shape=diamond];
"iCloud sync?" [shape=diamond];
"Existing CoreData?" [shape=diamond];
"Use SwiftData" [shape=box, style=filled, fillcolor=lightgreen];
"Use CoreData" [shape=box, style=filled, fillcolor=lightblue];
"Stay with CoreData" [shape=box, style=filled, fillcolor=lightblue];
"New iOS app?" -> "iOS 17+ only?" [label="yes"];
"New iOS app?" -> "Existing CoreData?" [label="no"];
"Existing CoreData?" -> "Stay with CoreData" [label="yes"];
"Existing CoreData?" -> "iOS 17+ only?" [label="no"];
"iOS 17+ only?" -> "Simple data model?" [label="yes"];
"iOS 17+ only?" -> "Use CoreData" [label="no"];
"Simple data model?" -> "iCloud sync?" [label="yes"];
"Simple data model?" -> "Use CoreData" [label="no"];
"iCloud sync?" -> "Use SwiftData" [label="yes"];
"iCloud sync?" -> "Use SwiftData" [label="no"];
}
SwiftData を選ぶ場合
- 新規アプリで、デプロイメントターゲットが iOS 17 以降
- シンプルなリレーションシップ(主に 1 対 1 または 1 対多で、複雑なクエリは少ない)
- SwiftUI を中心としたアーキテクチャ
- 最小限の設定でiCloud 同期が必要
CoreData を選ぶ場合
- iOS 16 以前のサポートが必要
- NSCompoundPredicate やサブクエリを使う複雑なクエリ
- NSFetchedResultsController を使用するUIKit 中心のアプリ
- 既存の CoreData コードベース(移行が複雑なため、CoreData を使い続ける)
- パフォーマンスが重要(2025 年時点では、CoreData のほうが依然として高速)
- 抽象エンティティまたは継承階層
移行時の重大な落とし穴
落とし穴 1:iCloud を使用する場合の未バージョンからバージョン管理への移行
重大: バージョン管理されていない SwiftData を iCloud 同期付きでリリースした場合、最初の移行時にユーザーのアプリがクラッシュします。
エラー: "Cannot use staged migration with an unknown model version"
理由: 同期の一貫性を保つため、CloudKit ではバージョン管理されたスキーマが必要です。
解決策:
// ❌ WRONG: Adding versioning after shipping unversioned + iCloud
// This will crash for all existing users
// ✅ CORRECT: Two-phase migration
// Version 1.1: Add versioning WITHOUT schema changes
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model
final class User {
var name: String
var email: String
// EXACT same schema as shipped v1.0
}
}
// Version 1.2: Now safe to add new properties
enum SchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model
final class User {
var name: String
var email: String
var createdAt: Date? // New property
}
}
ルール: 初日からバージョン管理されたスキーマを使用するか、まずバージョン管理用のラッパーをリリースしてください。
落とし穴 2:重複データがある状態での一意制約
状況: データに重複があるときに @Attribute(.unique) を追加する。
結果: 軽量移行が失敗し、アプリが機能しなくなります。
解決策: 制約を適用する前に重複を排除する複雑な移行を行います。
struct DeduplicateThenUnique: MigrationStage {
func migrate(context: ModelContext) throws {
let items = try context.fetch(FetchDescriptor<OldSchema.Product>())
let grouped = Dictionary(grouping: items, by: { $0.sku })
// Keep first, delete duplicates
for (_, duplicates) in grouped where duplicates.count > 1 {
for item in duplicates.dropFirst() {
context.delete(item)
}
}
try context.save()
}
}
ルール: 制約を適用する前にデータをクリーンアップしてください。本番データのバックアップを使ってテストしてください。
落とし穴 3:iCloud 同期による移行のブロック
重大: iCloud 同期を有効にすると、SwiftData は軽量移行との互換性を損なう移行をブロックします。
これが意味すること:
- プロパティ名を変更できません(同期が壊れます)
- リレーションシップの削除ルールを変更できません(同期が壊れます)
- 必須プロパティを削除できません(同期が壊れます)
回避策:
// Instead of renaming (blocked):
// @Model class User {
// var userName: String // Renamed from 'name'
// }
// Add new, deprecate old (allowed):
@Model class User {
var name: String // Keep for backward compatibility
var userName: String? // New preferred field
var displayName: String {
userName ?? name // Compute from both
}
}
ルール: iCloud を使う場合、追加のみの変更が許可されます。事前にスキーマを慎重に設計してください。
落とし穴 4:メインスレッドでの移行によるブロック
症状: アップデート後のアプリ起動時に、3~5 秒間画面が白くなります。
原因: 大規模なデータセットの移行がメインスレッドで実行されます。
解決策: バックグラウンドで初期化します。
// ✅ Initialize container on background thread
Task.detached {
let container = try await ModelContainer(
for: User.self,
migrationPlan: MigrationPlan.self
)
await MainActor.run {
self.container = container
}
}
ルール: 現実的なデータ量(1000 件以上のレコード)で必ず移行をテストしてください。
パフォーマンスパターン
パターン 1:バッチフェッチ
問題: 1000 件以上のオブジェクトを一度に読み込むと、メモリを消費し、UI がブロックされます。
解決策: fetchBatchSize を使って遅延読み込みします。
// ❌ WRONG: Loads all 1000 posts into memory
let request: NSFetchRequest<Post> = Post.fetchRequest()
let posts = try context.fetch(request)
// ✅ CORRECT: Loads 20 at a time as needed
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.fetchBatchSize = 20
let posts = try context.fetch(request)
ルール: フィード、リスト、ページ分割されたビューでは、必ず fetchBatchSize を設定してください。
パターン 2:BLOB の保存
問題: CoreData/SwiftData に画像や動画を保存すると、パフォーマンスが大幅に低下します。
ルール:
- 100KB 未満:インラインで保存(安全)
- 100KB~1MB:リレーションシップを持つ別エンティティに保存
- 1MB 超:ファイルシステムに保存し、データベースにはパスを保存
// ❌ WRONG: 2MB images in Post entity
@Model
class Post {
var title: String
@Attribute(.externalStorage) var image: Data // Still loads eagerly!
}
// ✅ CORRECT: File system storage
@Model
class Post {
var title: String
var imagePath: String? // "images/post-123.jpg"
}
// Load image on-demand
func loadImage(for post: Post) -> UIImage? {
guard let path = post.imagePath else { return nil }
let url = FileManager.documentsDirectory.appendingPathComponent(path)
return UIImage(contentsOfFile: url.path)
}
理由: データベースのフェッチでは、オブジェクト全体が読み込まれます。BLOB は表示されていない場合でもメモリ使用量を増大させます。
パターン 3:リレーションシップのフォールティング
問題: 投稿をフェッチすると、関連するすべてのユーザーや画像が即座に読み込まれます。
解決策: フォールトを返し、アクセス時に読み込みます。
// ✅ Relationships default to faults (good)
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.relationshipKeyPathsForPrefetching = [] // Don't prefetch
let posts = try context.fetch(request)
// Access triggers individual fault:
let authorName = posts[0].author.name // Only now loads User
// ✅ Prefetch when you know you'll need it:
request.relationshipKeyPathsForPrefetching = ["author"]
ルール: フェッチしたすべてのオブジェクトでアクセスするリレーションシップのみ、事前フェッチしてください。
パターン 4:NSFetchedResultsController(CoreData)
対象: ライブ更新に対応するテーブルビュー/コレクションビュー。
理由: 遅延読み込み、自動的な UI 更新、組み込みのセクション分けが利用できます。
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
request.fetchBatchSize = 20
let frc = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: "PostsCache"
)
frc.delegate = self
try frc.performFetch()
// UITableView uses frc.fetchedObjects
ルール: CoreData を使用するすべてのテーブルビュー/コレクションビューで FRC を使用してください。
SwiftData の制限事項(iOS 18 時点)
| 機能 | CoreData | SwiftData | 回避策 |
|---|---|---|---|
| NSCompoundPredicate | ✅ | ❌ | 複数のクエリ |
| NSFetchedResultsController | ✅ | ❌ | SwiftUI の @Query |
| 抽象エンティティ | ✅ | ❌ | プロトコルを使用 |
| 子コンテキスト | ✅ | ❌ | ModelContext を使用 |
| モデルのサブクラス化 | ✅ | ❌ | サブクラス化しない |
| パフォーマンス | 高速 | 低速 | アップデートを待つ |
よくある間違い
| 間違い | 実際の仕様 | 対処法 |
|---|---|---|
| 「マイグレーションは自動」 | 軽量マイグレーションのみです。一意制約や名前変更には複雑なマイグレーションが必要です。 | 本番データでテストする |
| 「バージョン管理は後で行う」 | バージョン管理なし + iCloud では、最初のマイグレーション時にクラッシュします | 最初からバージョン管理する |
| 「SwiftData は簡略化された CoreData」 | SwiftData には異なるルールがあります(サブクラス化不可、削除ルールも異なります) | SwiftData 固有の制約を理解する |
| 「externalStorage 属性で BLOB を解決できる」 | フェッチ時には引き続きメモリに読み込まれます | ファイルシステムに保存する |
| 「プロパティ名は自由に変更できる」 | iCloud を使っている場合、名前の変更はできません | 新しいプロパティを追加し、古いものを残す |
| 「マイグレーションで UI がブロックされることはない」 | 大規模なデータセットではメインスレッドがブロックされます | バックグラウンドで初期化する |
マイグレーションチェックリスト
スキーマ変更をリリースする前に:
- バージョン管理されたスキーマ(V1、V2)を定義した
- 複雑な場合(重複排除、名前変更)はマイグレーションステージを作成した
- 本番データのバックアップ(1000 件以上のレコード)でテストした
- バックグラウンドスレッドでマイグレーションをテストした(白い画面が表示されないこと)
- マイグレーション後に iCloud 同期が機能することを確認した
- 新しいプロパティにデフォルト値があるか、オプショナルであることを確認した
- 一意制約がクリーンなデータにのみ適用されることを確認した
クイックリファレンス
SwiftData:
// Basic model
@Model
class User {
@Attribute(.unique) var email: String
var name: String
@Relationship(deleteRule: .cascade) var posts: [Post]
}
// Versioned schemas
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model final class User { /* ... */ }
}
// Query (SwiftUI)
@Query(sort: \.createdAt, order: .reverse) var users: [User]
CoreData:
// Fetch with batching
let request: NSFetchRequest<User> = User.fetchRequest()
request.fetchBatchSize = 20
request.predicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText)
// Background context
let bgContext = container.newBackgroundContext()
bgContext.perform {
// Heavy work here
try? bgContext.save()
}
危険信号 — 作業を止めて再検討してください
- iCloud を有効にしたままバージョン管理なしのスキーマをリリースする → まずバージョン管理用ラッパーをリリースする
- 汚れたデータに一意制約を追加する → 重複排除マイグレーションを実行する
- 数 MB のファイルをデータベースに保存する → ファイルシステムに移動する
- アップデート後の起動時に白い画面が表示される → バックグラウンドでマイグレーションする
- フェッチに 500ms 超かかる → fetchBatchSize を追加し、BLOB がないか確認する
- 「ユーザーから苦情が来たらマイグレーションに対処する」 → 本番データで今すぐテストする
実際の効果
変更前: バージョン管理なしからバージョン管理ありへの移行で、最初のマイグレーション後に iCloud を有効にしたユーザー全員のアプリがクラッシュします。
変更後: まずバージョン管理用ラッパーを含む v1.1 をリリースし(スキーマ変更なし)、次に実際の変更を含む v1.2 をリリースします。クラッシュはゼロです。
変更前: 1000 件の投稿と、それぞれ 2MB の画像があるフィードの読み込みに 4 秒かかります。
変更後: 画像をファイルシステムに移動し、fetchBatchSize = 20 に設定します。フィードは 300ms で読み込まれ、画像は遅延読み込みされます。