mcpField
A
axiom-core-data

axiom-core-data

github.com/charleswiltgen開発1.2k170 回インストール今日更新
リポジトリ

axiom-core-data について

Swift 開発者向けの Core Data 実践リファレンスです。永続化、並行処理、リレーション、マイグレーションを扱い、旧バージョンの iOS や高度な要件、既存アプリでの安全な利用方法と注意点を説明します。

主な機能

  1. 01Core Data と SwiftData の選択指針
  2. 02並行処理とバックグラウンドコンテキスト
  3. 03リレーション設計とフェッチ
  4. 04軽量マイグレーションとマッピングモデル
  5. 05パフォーマンスとアンチパターンの指針

活用シーン

  1. 01旧バージョンの iOS アプリで永続化方式を選ぶ
  2. 02バックグラウンドインポートや非同期フェッチを扱う
  3. 03複雑なリレーションとスキーマ移行を計画する

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-core-data

インストール後、Claude Code で/axiom-core-dataを入力して読み込まれたか確認します。

詳細

Core Data

概要

基本原則:Core Data は成熟したオブジェクトグラフおよび永続化フレームワークです。SwiftData がサポートしていない機能が必要な場合や、古い iOS バージョンを対象にする場合に使用してください。

Core Data と SwiftData の使い分け:

  • SwiftData(iOS 17 以降)— 新しいアプリ、よりシンプルな API、Swift ネイティブ
  • Core Data — iOS 16 以前、高度な機能、既存のコードベース

すばやく判断するためのフローチャート

Which persistence framework?

├─ Targeting iOS 17+ only?
│  ├─ Simple data model? → SwiftData (recommended)
│  ├─ Need public CloudKit database? → Core Data (SwiftData is private-only)
│  ├─ Need custom migration logic? → Core Data (more control)
│  └─ Existing Core Data app? → Keep Core Data or migrate gradually
│
├─ Targeting iOS 16 or earlier?
│  └─ Core Data (SwiftData unavailable)
│
└─ Need both? → Use Core Data with SwiftData wrapper (advanced)

注意すべき兆候

次のいずれかが見られた場合は、作業を中断してください。

  • ❌ 「任意のスレッドで管理オブジェクトにアクセスする」— スレッド閉じ込め違反です
  • ❌ 「実機でのマイグレーションテストを省略する」— シミュレーターではスキーマの問題が見えません
  • ❌ 「どこでもシングルトンコンテキストを使う」— 並行処理によるクラッシュにつながります
  • ❌ 「常に軽量マイグレーションを強制する」— 複雑な変更にはマッピングモデルが必要です
  • ❌ 「ビューの本体でフェッチする」— @FetchRequest を使用するか、ビューモデルで監視してください

Core Data スタックのセットアップ

最新のスタック(iOS 10 以降)
import CoreData

class CoreDataStack {
    static let shared = CoreDataStack()

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Model")

        // Configure for CloudKit if needed
        // container.persistentStoreDescriptions.first?.cloudKitContainerOptions =
        //     NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.app")

        container.loadPersistentStores { description, error in
            if let error = error {
                // Handle appropriately for production
                fatalError("Failed to load store: \(error)")
            }
        }

        // Enable automatic merging
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        return container
    }()

    var viewContext: NSManagedObjectContext {
        persistentContainer.viewContext
    }

    func newBackgroundContext() -> NSManagedObjectContext {
        persistentContainer.newBackgroundContext()
    }
}
CloudKit との統合
import CoreData

class CloudKitStack {
    lazy var container: NSPersistentCloudKitContainer = {
        let container = NSPersistentCloudKitContainer(name: "Model")

        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("No store description")
        }

        // Enable CloudKit sync
        description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
            containerIdentifier: "iCloud.com.yourapp"
        )

        // Enable history tracking for sync
        description.setOption(true as NSNumber,
                             forKey: NSPersistentHistoryTrackingKey)
        description.setOption(true as NSNumber,
                             forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("CloudKit store failed: \(error)")
            }
        }

        container.viewContext.automaticallyMergesChangesFromParent = true

        return container
    }()
}

並行処理のパターン

最も重要なルール

NSManagedObject をスレッド間で渡してはいけません。 代わりに objectID を渡してください。

// ❌ WRONG: Passing object across threads
let user = viewContext.fetch(...)  // Main thread
Task.detached {
    print(user.name)  // CRASH: Wrong thread
}

// ✅ CORRECT: Pass objectID, fetch on target context
let userID = user.objectID
Task.detached {
    let bgContext = CoreDataStack.shared.newBackgroundContext()
    let user = bgContext.object(with: userID) as! User
    print(user.name)  // Safe
}
バックグラウンド処理
// ✅ CORRECT: Background context for heavy work
func importData(_ items: [ImportItem]) async throws {
    let context = CoreDataStack.shared.newBackgroundContext()

    try await context.perform {
        for item in items {
            let entity = Entity(context: context)
            entity.configure(from: item)
        }

        try context.save()
    }
}

// Changes automatically merge to viewContext if configured
Async/Await(iOS 15 以降)
// Modern async context operations
func fetchUsers() async throws -> [User] {
    let context = CoreDataStack.shared.viewContext

    return try await context.perform {
        let request = User.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        return try context.fetch(request)
    }
}

リレーションシップのモデリング

1 対多
// In User entity
@NSManaged var posts: NSSet?

// Convenience accessors
extension User {
    var postsArray: [Post] {
        (posts?.allObjects as? [Post]) ?? []
    }

    func addPost(_ post: Post) {
        mutableSetValue(forKey: "posts").add(post)
    }
}
多対多
// Both sides have NSSet
// User.tags <-> Tag.users

extension User {
    func addTag(_ tag: Tag) {
        mutableSetValue(forKey: "tags").add(tag)
        // Core Data automatically adds to tag.users
    }
}
削除ルール
ルール動作ユースケース
Nullifyリレーションシップを nil に設定するオプションのリレーションシップ
Cascade関連するオブジェクトを削除する所有する子オブジェクト(User → Posts)
Deny関連するオブジェクトが存在する場合、削除を防ぐ参照されているデータの保護
No Action何もしない(手動でのクリーンアップが必要)適用できるケースはまれ

フェッチのパターン

SwiftUI との統合
struct UserList: View {
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \User.name, ascending: true)],
        predicate: NSPredicate(format: "isActive == YES"),
        animation: .default
    )
    private var users: FetchedResults<User>

    var body: some View {
        List(users) { user in
            Text(user.name ?? "Unknown")
        }
    }
}

// Dynamic predicates
struct FilteredList: View {
    @FetchRequest var items: FetchedResults<Item>

    init(category: String) {
        _items = FetchRequest(
            sortDescriptors: [NSSortDescriptor(keyPath: \Item.date, ascending: false)],
            predicate: NSPredicate(format: "category == %@", category)
        )
    }
}
バッチフェッチ(N+1 の回避)
// ❌ WRONG: N+1 queries
let users = try context.fetch(User.fetchRequest())
for user in users {
    print(user.posts?.count ?? 0)  // Fault fired for each user
}

// ✅ CORRECT: Prefetch relationships
let request = User.fetchRequest()
request.relationshipKeyPathsForPrefetching = ["posts"]
let users = try context.fetch(request)
for user in users {
    print(user.posts?.count ?? 0)  // Already loaded
}
大規模データセット向けのバッチサイズ
let request = User.fetchRequest()
request.fetchBatchSize = 20  // Load 20 at a time as needed
request.returnsObjectsAsFaults = true  // Default, memory efficient

スキーママイグレーション

軽量マイグレーション(自動)

次の変更は自動的に処理されます。

  • オプショナル属性の追加
  • 属性の削除
  • 名前の変更(名前変更識別子を使用)
  • オプショナル値またはデフォルト値を持つリレーションシップの追加
let description = NSPersistentStoreDescription()
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
マッピングモデルが必要な場合
  • 属性の型の変更
  • エンティティの分割/統合
  • 複雑なリレーションシップの変更
  • マイグレーション中のデータ変換
// Create mapping model in Xcode:
// File → New → Mapping Model
// Select source and destination models
マイグレーションのテストチェックリスト

リリース前に必ず実施してください:

  1. ✓ 実機でテストする(再ビルド時にシミュレーターはデータベースを削除します)
  2. ✓ 古いバージョンをインストールし、データを作成する
  3. ✓ その上に新しいバージョンをインストールする
  4. ✓ すべてのデータにアクセスできることを確認する
  5. ✓ マイグレーションのパフォーマンスを確認する(大規模なデータセット)

アンチパターン

1. すべてにシングルトンコンテキストを使う
// ❌ WRONG: One context for all operations
class DataManager {
    let context = CoreDataStack.shared.viewContext

    func importInBackground() {
        // Using main context on background = crash
        for item in largeDataset {
            let entity = Entity(context: context)
        }
    }
}

// ✅ CORRECT: Context per operation type
func importInBackground() {
    let bgContext = CoreDataStack.shared.newBackgroundContext()
    bgContext.perform {
        // Safe background work
    }
}
2. ビューの本体でフェッチする
// ❌ WRONG: Fetch on every render
var body: some View {
    let users = try? context.fetch(User.fetchRequest())  // Called repeatedly!
    List(users ?? []) { ... }
}

// ✅ CORRECT: Use @FetchRequest
@FetchRequest(sortDescriptors: [])
var users: FetchedResults<User>

var body: some View {
    List(users) { ... }  // Automatic updates
}
3. マージポリシーを無視する
// ❌ WRONG: No merge policy (conflicts crash)
let context = container.viewContext

// ✅ CORRECT: Define merge behavior
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true

パフォーマンスのヒント

  1. 大規模な結果セットには fetchBatchSize を使用する
  2. アクセスする予定のリレーションシップを 事前にフェッチする
  3. インポート/エクスポートには バックグラウンドコンテキストを使用する
  4. まとめて保存する — 挿入のたびに保存しない
  5. 最初の N 件の結果だけが必要な場合は fetchLimit を使用する
  6. SQL デバッグを有効にしてプロファイルする:-com.apple.CoreData.SQLDebug 1

プレッシャーのかかる状況

シナリオ 1:「SwiftData のほうがシンプルなので、今すぐ移行しましょう」

状況:iOS 17 の新機能が利用可能になり、プロジェクトの途中で移行したくなります。

リスク:マイグレーションは複雑です。Core Data と SwiftData の併用には落とし穴があります。

対応:「まず現在のマイルストーンを完了してください。移行には専用の時間とテストが必要です。」

シナリオ 2:「シミュレーターで動くので、マイグレーションのテストは省略しましょう」

状況:シミュレーターだけでスキーマ変更をテストしています。

リスク:再ビルド時にシミュレーターはデータベースを削除します。実機ではデータが保持されるため、クラッシュする可能性があります。

対応:「必須です。実データを使って実機でテストしてください。今の 15 分で本番環境でのクラッシュを防げます。」

tvOS

tvOS では CoreData + CloudKit は危険です。 CloudKit のメタデータによってローカルストアの容量が大幅に増加します。また、tvOS には永続的なローカルストレージがなく、システムはいつでも Caches(Application Support を含む)を削除します。容量が増加したストアと予測できない削除が重なるのは、最悪の組み合わせです。

推奨事項:tvOS のデータ永続化には、代わりに SQLiteData と CloudKit SyncEngine を使用してください。tvOS のストレージに関する制約について詳しくは、axiom-tvos を参照してください。

関連スキル

  • axiom-core-data-diag — マイグレーション、スレッドエラー、N+1 クエリのデバッグ
  • axiom-swiftdata — iOS 17 以降向けのモダンな代替手段
  • axiom-database-migration — 安全なスキーマ進化のパターン
  • axiom-swift-concurrency — Core Data での Async/await パターン

コメント