最終更新: 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日溶かした経験があるので、なおさら)。
| 項目 | SwiftData | Core Data |
|---|---|---|
| モデル定義 | @Model付きSwiftクラス | .xcdatamodeld + 生成クラス |
| クエリ | #Predicate(型安全) | NSPredicate(文字列) |
| SwiftUI統合 | @Queryプロパティラッパー | @FetchRequest |
| 並行処理 | ModelActor(Swift 6 Sendable準拠) | performクロージャ |
| CloudKit同期 | 1行の設定で有効 | NSPersistentCloudKitContainerで複雑 |
| スキーマ移行 | VersionedSchema + MigrationPlan | マッピングモデル or 軽量移行 |
| 最低対応OS | iOS 17 / macOS 14 / visionOS 1 | iOS 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段階アプローチが最も安全です(私も直近のプロジェクトでこの順番でやりました)。
- 共存モード: 既存の
.xcdatamodeldと同じスキーマ形状で@Modelクラスを新規作成し、ModelConfigurationで既存のSQLiteファイルを指定する。両方のAPIから同じデータが読み書きできる状態を作る。 - 読み取り側の切り替え: 新規機能や画面から順に
@Queryベースに置き換える。書き込みは既存Core Dataコードのままで問題ない。 - 完全移行: すべての読み書きが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を使ってフィルタしてください。




