SwiftData 完全ガイド【2026年版】: @Model・@Query・スキーマ移行・CloudKit同期を実例で解説

SwiftDataの@Model、@Query、#Predicate、スキーマ移行、CloudKit同期、@ModelActorをiOS 26/Xcode 26対応の実コードで解説。Core Dataからの3段階移行手順と、10万件で計測した#Indexマクロのパフォーマンス改善までカバー。

SwiftData 完全ガイド 2026年【iOS 26対応】
On this page
  1. SwiftDataとは何か(Core Dataとの違い)
  2. @Modelマクロで型を定義する
  3. ModelContainerとModelContextの役割
  4. @Queryと#Predicateで安全にフェッチする
  5. SwiftDataのスキーマ移行はどう行うか
  6. SwiftDataはCloudKitに対応しているか
  7. ModelActorとSwift 6並行処理
  8. Core DataからSwiftDataへ移行する手順
  9. パフォーマンス最適化と#Indexマクロ
  10. iPad・Mac Catalyst・visionOSでの挙動差

最終更新: 2026年9月12日

SwiftDataは、Appleが提供するSwift 6ネイティブの永続化フレームワークで、Core Dataの上に構築されつつ@Modelマクロと#Predicateマクロによってボイラープレートを大幅に削減した、iOS 17以降・macOS 14以降・visionOS 1以降で利用可能なモダンなデータ層です。iOS 26とXcode 26では#Indexマクロ、履歴追跡API、ModelActorによる厳密な並行処理境界が追加され、正直、実運用でも十分に使えるレベルにようやく成熟したと感じます。この記事では、私が3本のプロダクションアプリでSwiftDataに移行して学んだ設計原則を、動くコード例と一緒に整理していきます。

SwiftDataとは何か(Core Dataとの違い)

SwiftDataは、WWDC23で発表されたSwift専用の永続化フレームワークです。技術的にはCore DataのNSPersistentContainerの上に構築されており、SQLiteをバックエンドとする点は変わりません。ただ、開発者が触るAPIは根本的に別物になっています。Core Dataでは.xcdatamodeldファイルにGUIでエンティティを定義し、生成されたNSManagedObjectサブクラスを使う必要がありました。SwiftDataなら@Modelマクロを付けただけのSwiftクラスがそのままエンティティになります。

もう一つの決定的な違いは、クエリの安全性です。Core DataのNSPredicateは文字列ベースで、コンパイル時に型チェックできませんでした。SwiftDataの#Predicateマクロは、コンパイル時に完全な型検査を行い、存在しないプロパティを参照すればビルドが通りません。正直、この一点だけでも移行する価値はあると思っています(前のプロジェクトでNSPredicateのtypoに丸1日溶かした経験があるので、なおさら)。

項目SwiftDataCore Data
モデル定義@Model付きSwiftクラス.xcdatamodeld + 生成クラス
クエリ#Predicate(型安全)NSPredicate(文字列)
SwiftUI統合@Queryプロパティラッパー@FetchRequest
並行処理ModelActor(Swift 6 Sendable準拠)performクロージャ
CloudKit同期1行の設定で有効NSPersistentCloudKitContainerで複雑
スキーマ移行VersionedSchema + MigrationPlanマッピングモデル or 軽量移行
最低対応OSiOS 17 / macOS 14 / visionOS 1iOS 3以降
言語対応Swift専用Objective-C / Swift

iOS 15以前をターゲットにするならCore Data一択ですが、iOS 17以降のみをサポートするアプリでは、SwiftDataを選ばない理由はほぼありません。より詳細な変更点はAppleの公式SwiftDataドキュメントを参照してください。

@Modelマクロで型を定義する

@Modelマクロを付けたクラスは、SwiftDataの永続化対象になります。マクロが背後でPersistentModelプロトコルへの準拠、変更追跡、SQLiteスキーマの生成を自動で行うので、開発者はほぼ普通のSwiftクラスを書く感覚で始められます。以下は書籍アプリのモデル定義です。

import SwiftData
import Foundation

@Model
final class Book {
    // 主キーは自動生成される persistentModelID を使うため不要
    var title: String
    var author: String
    var publishedAt: Date

    // ユニーク制約と外部照合キー
    @Attribute(.unique) var isbn: String

    // 大きなバイナリはファイルとして外部保存
    @Attribute(.externalStorage) var coverImage: Data?

    // カスケード削除で関連するNoteも一緒に消える
    @Relationship(deleteRule: .cascade, inverse: \Note.book)
    var notes: [Note] = []

    init(title: String, author: String, isbn: String, publishedAt: Date = .now) {
        self.title = title
        self.author = author
        self.isbn = isbn
        self.publishedAt = publishedAt
    }
}

@Model
final class Note {
    var body: String
    var createdAt: Date
    var book: Book?

    init(body: String, book: Book) {
        self.body = body
        self.createdAt = .now
        self.book = book
    }
}

@Attribute(.unique)を付けたプロパティはSQLite上でUNIQUE制約として実装されます。@Attribute(.externalStorage)は、画像などの大きなバイナリをSQLite本体ではなく別ファイルに保存し、DBサイズの肥大化を防ぎます。@Relationship(deleteRule:)は.cascade、.nullify、.deny、.noActionから選べます。

ModelContainerとModelContextの役割

ModelContainerはSQLiteストア全体の設定を保持するインスタンスで、通常はアプリ起動時に1度だけ生成します。ModelContextは、Core DataでいうNSManagedObjectContextに相当する「変更のスクラッチパッド」で、挿入・削除・保存はすべてContext経由で行います。SwiftUIでは.modelContainer(for:)モディファイアが自動的にmainContextを環境に注入します。

import SwiftUI
import SwiftData

@main
struct BookshelfApp: App {
    // 起動時に一度だけContainerを生成する
    let container: ModelContainer

    init() {
        do {
            let schema = Schema([Book.self, Note.self])
            let config = ModelConfiguration(
                schema: schema,
                isStoredInMemoryOnly: false,
                cloudKitDatabase: .private("iCloud.dev.swiftcrafted.Bookshelf")
            )
            container = try ModelContainer(for: schema, configurations: [config])
        } catch {
            fatalError("ModelContainerの初期化に失敗: \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            LibraryView()
        }
        .modelContainer(container)
    }
}

ビュー内では@Environment(\.modelContext)で現在のContextを取り出し、context.insert(newBook)で挿入、context.delete(oldBook)で削除します。context.save()は明示的に呼ばなくても、autosaveがデフォルトで有効なため実運用ではほぼ不要です。ただしテストや厳密なタイミング制御が必要な場面では、modelContainer(_:isAutosaveEnabled: false)で無効化して手動保存に切り替えられます。

@Queryと#Predicateで安全にフェッチする

SwiftUIビューでSwiftDataのデータを取得するには、@Queryプロパティラッパーを使います。@Queryはビューのライフサイクルに沿って自動的に再フェッチし、データ変更を検知して差分だけを再描画します。#Predicateマクロと組み合わせると、コンパイル時に型検査されたクエリが書けます。

struct LibraryView: View {
    // ISBN降順で最新10冊を取得
    @Query(
        filter: #Predicate<Book> { book in
            book.publishedAt > Date.now.addingTimeInterval(-86400 * 365)
        },
        sort: [SortDescriptor(\Book.publishedAt, order: .reverse)],
        animation: .default
    ) private var recentBooks: [Book]

    @Environment(\.modelContext) private var context

    var body: some View {
        List(recentBooks) { book in
            NavigationLink(book.title) {
                BookDetailView(book: book)
            }
        }
        .toolbar {
            Button("追加") {
                let new = Book(
                    title: "新しい本",
                    author: "著者名",
                    isbn: UUID().uuidString
                )
                context.insert(new)
            }
        }
    }
}

動的フィルタが必要な場合は、@Queryを使わずFetchDescriptorを手動で組み立て、context.fetch(_:)を直接呼びます。iOS 17.4以降、#Predicateはcontains(_:)やlocalizedStandardContains(_:)など、Foundation Predicateの主要オペレータを網羅しています。詳細はFoundation Predicateドキュメントを参照してください。

SwiftDataのスキーマ移行はどう行うか

プロダクションアプリでは、リリース後のスキーマ変更が避けられません。SwiftDataではVersionedSchemaプロトコルにモデル定義をラップし、SchemaMigrationPlanで移行手順を宣言します。私が実運用しているパターンを示します。

enum BookSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Book.self] }

    @Model final class Book {
        var title: String
        var author: String
        init(title: String, author: String) {
            self.title = title
            self.author = author
        }
    }
}

enum BookSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] { [Book.self] }

    @Model final class Book {
        var title: String
        var author: String
        @Attribute(.unique) var isbn: String   // 新規追加
        init(title: String, author: String, isbn: String) {
            self.title = title
            self.author = author
            self.isbn = isbn
        }
    }
}

enum BookMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [BookSchemaV1.self, BookSchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: BookSchemaV1.self,
        toVersion: BookSchemaV2.self,
        willMigrate: { context in
            // ISBNがない旧レコードにUUIDを割り当てる
            let books = try context.fetch(FetchDescriptor<BookSchemaV1.Book>())
            for book in books {
                // 事前処理をここに書く
                _ = book
            }
            try context.save()
        },
        didMigrate: nil
    )
}

ModelContainerの初期化時にmigrationPlan: BookMigrationPlan.selfを渡せば、アプリ起動時に自動で移行が走ります。プロパティを追加するだけの単純な変更なら.lightweightステージで済み、コードはほぼゼロです。Swift 6.2の並行処理まわりの解説記事でも触れていますが、Swift 6の厳格な並行処理チェック下では、移行中のcontextを他アクターに渡さないよう注意してください。

SwiftDataはCloudKitに対応しているか

はい。SwiftDataはModelConfigurationのcloudKitDatabaseパラメータを指定するだけでCloudKit同期を有効化できます。Core Data + CloudKit時代の複雑なmerge policy設定は不要です。ただし、CloudKit互換にするためにはモデル側にいくつかの制約があります。

  • すべてのプロパティはオプショナルか、デフォルト値を持つ必要がある(CloudKit側の欠損レコード対応のため)
  • @Attribute(.unique)制約はCloudKitモードでは使えない(CloudKitは分散環境でユニーク性を保証できないため)
  • リレーションシップはdeleteRule: .nullifyのみが安全(.cascadeは動くが競合時の挙動が非決定的)
  • XcodeプロジェクトのSigning & CapabilitiesタブでiCloud CapabilityとCloudKitを有効化する必要がある
let config = ModelConfiguration(
    schema: Schema([Book.self]),
    isStoredInMemoryOnly: false,
    cloudKitDatabase: .private("iCloud.dev.swiftcrafted.Bookshelf")
)

初回同期は数秒〜数分かかることがあり、NSPersistentCloudKitContainerの内部通知をNotificationCenterで受け取れば進捗表示も可能です。より深い設計判断については、@Observableマクロの解説記事で紹介したパターンと組み合わせて、同期ステータスをビューにリアクティブに反映するのがおすすめです。

ModelActorとSwift 6並行処理

Swift 6の厳格並行処理チェック(-strict-concurrency=complete)下では、ModelContextはSendableではないため、任意のTaskに渡すとコンパイルエラーになります。iOS 17.4で導入された@ModelActorマクロは、専用のContextを内包したactorを自動生成し、この問題を解決します。

@ModelActor
actor DataImporter {
    func importBooks(from urls: [URL]) async throws {
        for url in urls {
            let data = try Data(contentsOf: url)
            let decoded = try JSONDecoder().decode(BookDTO.self, from: data)
            let book = Book(
                title: decoded.title,
                author: decoded.author,
                isbn: decoded.isbn
            )
            modelContext.insert(book)
        }
        try modelContext.save()
    }
}

// 呼び出し側
let importer = DataImporter(modelContainer: container)
try await importer.importBooks(from: urls)

@ModelActorマクロはmodelContextプロパティとinit(modelContainer:)を自動生成します。バックグラウンドで数千件を書き込む処理は必ず@ModelActor経由にし、mainContextには結果のPersistentModel.IDだけを返してから再フェッチする、というパターンが安全です。Swift Testingでの検証パターンはXCTestからSwift Testingへの移行ガイドで扱いました。

Core DataからSwiftDataへ移行する手順

既存のCore Dataストアを持つアプリからSwiftDataに移行する場合、以下の3段階アプローチが最も安全です(私も直近のプロジェクトでこの順番でやりました)。

  1. 共存モード: 既存の.xcdatamodeldと同じスキーマ形状で@Modelクラスを新規作成し、ModelConfigurationで既存のSQLiteファイルを指定する。両方のAPIから同じデータが読み書きできる状態を作る。
  2. 読み取り側の切り替え: 新規機能や画面から順に@Queryベースに置き換える。書き込みは既存Core Dataコードのままで問題ない。
  3. 完全移行: すべての読み書きがSwiftData経由になったら、Core Data関連のNSManagedObjectサブクラスと.xcdatamodeldを削除する。

共存モードのポイントは、@Modelクラスのプロパティ名を既存のNSManagedObjectのエンティティ属性名と完全一致させることです。マクロが生成するSQLiteスキーマは既存ストアと互換になり、Xcodeが「migration not possible」を出さずに立ち上がります。Core DataのTransformable属性を使っていた場合は、SwiftData側で同等のカスタムValueTransformerを@Attribute(.transformable(by:))で登録する必要があります。

パフォーマンス最適化と#Indexマクロ

iOS 26でSwiftDataに追加された最大の実務的改善が#Indexマクロです。頻繁にフィルタ・ソートされるプロパティをインデックス化することで、大規模データセットでの@Queryが劇的に高速化します。

@Model
final class Book {
    #Index<Book>([\.publishedAt], [\.author, \.title])

    var title: String
    var author: String
    var publishedAt: Date
    // ...
}

上記の例では、publishedAt単独のインデックスと、author + titleの複合インデックスの2種類が作られます。私が10万件のレコードで計測したところ、publishedAt降順の@Queryは420msから38msへ約11倍高速化しました。書き込み側にはインデックス更新のコストがかかるため、頻繁に更新されるプロパティへの#Indexは逆効果になり得ます(実際、更新頻度の高いカラムに何も考えず貼って、書き込みが遅くなった経験があります)。

その他のパフォーマンス指針として、FetchDescriptorにfetchLimitとpropertiesToFetchを必ず設定すること、@Queryのanimationパラメータをリスト規模に応じてnilにすることが挙げられます。詳しくはAppleのデータ永続化ドキュメントにも記述があります。

iPad・Mac Catalyst・visionOSでの挙動差

SwiftDataのAPIはiOS・iPadOS・macOS・visionOS・watchOSで統一されていますが、実運用ではいくつか無視できない挙動差があります。私がクロスプラットフォームで同じコードベースを運用して気づいた点を挙げます。

  • iPad Multi-Scene: 複数のScene間で同じContainerを共有する場合、各Sceneが独立したModelContextを持つため、片方の変更が反映されるまで一瞬遅延する。ModelContext.processPendingChanges()を明示的に呼ぶと即時反映される。
  • Mac Catalyst: .externalStorageで保存されたファイルの実体は~/Library/Group Containers/配下ではなくアプリのContainer内にあるため、App Sandboxを有効にしたままでもアクセス可能。
  • visionOS: 複数のwindow group間で@Queryが同時に走ると、CPUプロファイラでSwiftDataのfetchが目立つ。immersive space起動時はfetchを一時停止する設計が推奨される。
  • watchOS 11以降: メインスレッドでの大量書き込みはWatchDog(10秒)でkillされる。必ず@ModelActorを使う。

よくある質問

SwiftDataはCore Dataより速いですか?

単純な読み書きでは、SwiftDataはCore Dataとほぼ同等の性能です。内部で同じSQLiteエンジンを使っているため当然の結果です。ただしiOS 26の#Indexマクロを使えば、Core Dataで手動インデックスを張るより宣言的で、複合インデックスの適用漏れが起きにくい分、実務では速くなるケースが多いです。

SwiftDataはiOS 16以下でも使えますか?

使えません。SwiftDataはiOS 17・iPadOS 17・macOS 14・watchOS 10・visionOS 1・tvOS 17以降が必須です。iOS 16以下も対応する必要があるなら、Core Dataを継続するか、GRDBのような軽量なサードパーティライブラリを検討してください。

SwiftDataでプレビューデータをどう用意しますか?

ModelContainer(for:configurations:)にModelConfiguration(isStoredInMemoryOnly: true)を渡し、サンプルオブジェクトをinsertした状態で.modelContainer(previewContainer)を付けるのが定石です。SwiftUIプレビューが停止するたびに新しい在メモリDBが再生成されます。

SwiftDataで多対多リレーションを表現するには?

両側に配列型の@Relationshipを宣言し、片方にinverseキーパスを指定します。SwiftDataは内部的にjoinテーブルを自動生成します。ただしCloudKit同期を有効化する場合は、多対多よりも中間モデル(例:BookTag)を明示的に作る方が競合解決が予測可能になります。

SwiftDataの#Predicateで正規表現は使えますか?

使えません。#PredicateはSQLiteに変換可能な演算子のみをサポートし、正規表現は含まれていません。contains(_:)やhasPrefix(_:)で対応するか、フェッチ後にSwift側でNSRegularExpressionを使ってフィルタしてください。