mcpField
S
swiftdata-coredata-persistence

swiftdata-coredata-persistence

github.com/dagba開発444 回インストール40 週間前に更新
リポジトリ

swiftdata-coredata-persistence について

swiftdata-coredata-persistenceは、SwiftDataとCoreDataの選択や運用を支援するiOS開発者向けのリファレンススキルです。フレームワークの選定、移行時のリスク、iCloudのスキーマ制約、データ取得と保存のパターンを扱います。

主な機能

  1. 01SwiftDataとCoreDataの適用場面を比較
  2. 02バージョン管理と複雑な移行のリスクを説明
  3. 03iCloud同期時のスキーマ制約を整理
  4. 04バッチ取得とリレーション読み込みのパターン
  5. 05大容量データのファイル保存を推奨
  6. 06移行チェックリストとよくある問題を掲載

活用シーン

  1. 01新しいiOSアプリの永続化方式を選ぶ
  2. 02iCloud同期を伴うスキーマ移行を計画
  3. 03起動遅延や移行失敗を調査
  4. 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 時点)

機能CoreDataSwiftData回避策
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 で読み込まれ、画像は遅延読み込みされます。

コメント