mcpField
S
swiftdata

swiftdata

github.com/dpearson2699データ・データベース1.2k4.2k 回インストール13 週間前に更新
リポジトリ

swiftdata について

iOS 開発者向けの SwiftData 永続化スキルです。モデル、クエリ、マイグレーション、並行処理、SwiftUI 連携に加え、CloudKit 同期と Core Data との共存も扱います。

主な機能

  1. 01モデル定義とコンテナ設定
  2. 02CRUD と述語クエリの実行
  3. 03スキーマのバージョン管理と移行
  4. 04@ModelActor による並行処理
  5. 05SwiftUI と @Query の連携
  6. 06CloudKit 同期設定の確認

活用シーン

  1. 01iOS アプリの永続化モデル設計
  2. 02Core Data からの段階的な移行
  3. 03CloudKit のスキーマ互換性の確認
  4. 04バックグラウンドでの安全なデータ操作

インストール

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

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftdata

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
core-data-coexistence.md
indexing.md
predicate-pitfalls.md
swiftdata-advanced.md
swiftdata-queries.md

詳細

SwiftData

Swift 6.3を使用して、iOS 26以降のアプリで構造化データを永続化、クエリ、管理します。

目次

モデル定義

@Model は class(struct ではなく)に適用します。PersistentModel への準拠が自動生成されます。モデルのインスタンスはコンテキスト/アクターに紐付いたままです。アクター間で受け渡すのはインスタンスではなく、その PersistentIdentifier です。

@Model
class Trip {
    var name: String
    var destination: String
    var startDate: Date
    var endDate: Date
    var isFavorite: Bool = false
    @Attribute(.externalStorage) var imageData: Data?
    @Relationship(deleteRule: .cascade, inverse: \LivingAccommodation.trip)
    var accommodation: LivingAccommodation?
    @Transient var isSelected: Bool = false  // Always provide default

    init(name: String, destination: String, startDate: Date, endDate: Date) {
        self.name = name; self.destination = destination
        self.startDate = startDate; self.endDate = endDate
    }
}

@Attribute のオプション:.externalStorage、.unique、.spotlight、.allowsCloudEncryption、.preserveValueOnDeletion、.ephemeral、.transformable(by:)。名前を変更する場合:@Attribute(originalName: "old_name")。

@Relationship:deleteRule: は .cascade/.nullify(デフォルト)/.deny/.noAction を指定します。確実に動作させるには inverse: を指定してください。単方向(iOS 18以降):inverse: nil。

#Unique(iOS 18以降):#Unique<Person>([\.firstName, \.lastName]) -- 複合一意性を指定します。

継承(iOS 26以降):@Model class BusinessTrip: Trip { var company: String }。

サポートされる型:Bool、Int/UInt の各種型、Float、Double、String、Date、Data、URL、UUID、Decimal、Array、Dictionary、Set、Codable に準拠する列挙型、Codable に準拠する構造体および互換性のあるその他の Codable 値型、ならびに @Model クラスへのリレーションシップ。

ModelContainer のセットアップ

// Basic
let container = try ModelContainer(for: Trip.self, LivingAccommodation.self)

// Configured
let config = ModelConfiguration("Store", isStoredInMemoryOnly: false,
    groupContainer: .identifier("group.com.example.app"),
    cloudKitDatabase: .private("iCloud.com.example.app"))
let container = try ModelContainer(for: Trip.self, configurations: config)

// With migration plan
let container = try ModelContainer(for: SchemaV2.Trip.self,
    migrationPlan: TripMigrationPlan.self)

// In-memory (previews/tests)
let container = try ModelContainer(for: Trip.self,
    configurations: ModelConfiguration(isStoredInMemoryOnly: true))

CloudKit の同期

ModelConfiguration(..., cloudKitDatabase:) を指定すると、SwiftData のストアで CloudKit との自動同期が有効になりますが、同期はアプリのエンタイトルメントによって制御されます。

SwiftData の CloudKit のセットアップやスキーマレビューを行う場合は、スキーマに関する指摘とは別に、必ず Capabilities の判定を含めてください。

  • Capabilities:Xcode のターゲットで CloudKit が有効な iCloud ケイパビリティを設定し、使用するコンテナを選択していることに加え、「Background Modes > Remote notifications」が設定されている必要があります。これらのエンタイトルメントがなければ、cloudKitDatabase を設定していても自動同期は完全には構成されていません。
  • スキーマの互換性:@Attribute(.unique) と #Unique は使用しないでください。リレーションシップはオプショナルにし、必要に応じて明示的な逆リレーションシップを設定し、.deny は避けてください。大きな Data には @Attribute(.externalStorage) を使用してください。
  • スカラー属性:CloudKit のためだけに、すべてのスカラーをオプショナルにしないでください。イニシャライザー、デフォルト値、または移行によって有効な値が設定される必須スカラーは、非オプショナルのままにしてください。
  • スキーマの展開:開発用スキーマの初期化は非本番ビルドでのみ行い、CloudKit Dashboard で検証してからリリース前に昇格させてください。本番環境の変更は追加のみとしてください。

CRUD 操作

破壊的な一括処理や移行を行う際は、まず破棄可能なコピーに対して正確な述語またはバージョン間の移行を実行し、影響を受ける識別子と件数を記録してください。トランザクションと保存のセマンティクスを明示して実行し、再フェッチして値、リレーションシップ、件数、不変条件を検証します。失敗した場合は述語/スキーマを修正し、クリーンなフィクスチャを復元してから再試行してください。破壊的な操作を無確認で再実行しないでください。

// CREATE
let trip = Trip(name: "Summer", destination: "Paris", startDate: .now, endDate: .now + 86400*7)
modelContext.insert(trip)
try modelContext.save()  // or rely on autosave

// READ
let trips = try modelContext.fetch(FetchDescriptor<Trip>(
    predicate: #Predicate { $0.destination == "Paris" },
    sortBy: [SortDescriptor(\.startDate)]))

// UPDATE -- modify properties directly; autosave handles persistence
trip.destination = "Rome"

// DELETE
modelContext.delete(trip)
try modelContext.delete(model: Trip.self, where: #Predicate { $0.isFavorite == false })

// TRANSACTION (atomic)
try modelContext.transaction {
    modelContext.insert(trip); trip.isFavorite = true
}

@Query in SwiftUI

struct TripListView: View {
    @Query(filter: #Predicate<Trip> { $0.isFavorite == true },
           sort: \.startDate, order: .reverse)
    private var favorites: [Trip]

    var body: some View { List(favorites) { trip in Text(trip.name) } }
}

// Dynamic query via init
struct SearchView: View {
    @Query private var trips: [Trip]
    init(search: String) {
        _trips = Query(filter: #Predicate<Trip> { trip in
            search.isEmpty || trip.name.localizedStandardContains(search)
        }, sort: [SortDescriptor(\.name)])
    }
    var body: some View { List(trips) { trip in Text(trip.name) } }
}

// FetchDescriptor query
struct RecentView: View {
    static var desc: FetchDescriptor<Trip> {
        var d = FetchDescriptor<Trip>(sortBy: [SortDescriptor(\.startDate)])
        d.fetchLimit = 5; return d
    }
    @Query(RecentView.desc) private var recent: [Trip]
    var body: some View { List(recent) { trip in Text(trip.name) } }
}

#Predicate

#Predicate<Trip> { $0.destination.localizedStandardContains("paris") }  // String
let now = Date()
#Predicate<Trip> { $0.startDate > now }                                 // Date
#Predicate<Trip> { $0.isFavorite && $0.destination != "Unknown" }       // Compound
#Predicate<Trip> { $0.accommodation?.name != nil }                      // Optional
#Predicate<Trip> { $0.tags.contains { $0.name == "adventure" } }        // Collection

サポート対象:==、!=、<、<=、>、>=、&&、||、!、contains()、allSatisfy()、filter()、starts(with:)、localizedStandardContains()、caseInsensitiveCompare()、算術演算、条件式、オプショナルチェーンとバインディング、nil 合体、型キャスト。避けるもの:ループ、ネストした宣言、変更操作、未サポートの任意のメソッド呼び出し。

FetchDescriptor

var d = FetchDescriptor<Trip>(predicate: ..., sortBy: [...])
d.fetchLimit = 20; d.fetchOffset = 0
d.includePendingChanges = true
d.propertiesToFetch = [\.name, \.startDate]
d.relationshipKeyPathsForPrefetching = [\.accommodation]
let trips = try modelContext.fetch(d)
let count = try modelContext.fetchCount(d)
let ids = try modelContext.fetchIdentifiers(d)
try modelContext.enumerate(d, batchSize: 1000) { trip in trip.isProcessed = true }

スキーマのバージョン管理と移行

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Trip.self] }
    @Model class Trip { var name: String; init(name: String) { self.name = name } }
}

enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] { [Trip.self] }
    @Model class Trip {
        var name: String; var startDate: Date?  // New property
        init(name: String) { self.name = name }
    }
}

enum TripMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] }
    static var stages: [MigrationStage] { [migrateV1toV2] }
    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
}

// Custom migration for data transformation
static let migrateV2toV3 = MigrationStage.custom(
    fromVersion: SchemaV2.self, toVersion: SchemaV3.self,
    willMigrate: nil,
    didMigrate: { context in
        let trips = try context.fetch(FetchDescriptor<SchemaV3.Trip>())
        for trip in trips { trip.displayName = trip.name.capitalized }
        try context.save()
    })

軽量な変更:省略可能またはデフォルト値付きのプロパティの追加、名前の変更(originalName)、プロパティの削除、モデル型の追加。 ステージのリストがサポート対象のすべてのバージョン間の移行を網羅していることを確認し、リリース前に古い各ストアの新しいコピーを移行して、移行後のデータを検証してください。

Core Data との共存境界

既存の Core Data ストアと SwiftData を併用する作業や、Core Data から SwiftData へ画面を段階的に移行する作業では、このスキルを使用してください。純粋な Core Data スタックのセットアップ、NSManagedObjectContext、NSFetchRequest、および Core Data のバッチ操作は、関連する core-data スキルで扱ってください。

共存させる場合は、詳しい移行方法を説明する前に、境界について案内してください。

  • SwiftData と Core Data が同じ SQLite ストア URL を参照するようにします。
  • SwiftData の @Model 定義で、Core Data のエンティティ名、プロパティ名、型、リレーションシップの形状を一致させます。
  • 永続化された Core Data の名前と Swift の名前が異なる SwiftData プロパティには、@Attribute(originalName:) を使用します。
  • 同じエンティティを両方のスタックから同時に書き込まないでください。移行中は、各エンティティの書き込み元となるスタックを 1 つに定めます。

並行処理(@ModelActor)

@ModelActor
actor DataHandler {
    func importTrips(_ records: [TripRecord]) throws {
        for r in records {
            modelContext.insert(Trip(name: r.name, destination: r.dest,
                                    startDate: r.start, endDate: r.end))
        }
        try modelContext.save()  // Always save explicitly in @ModelActor
    }

    func process(tripID: PersistentIdentifier) throws {
        guard let trip = self[tripID, as: Trip.self] else { return }
        trip.isProcessed = true; try modelContext.save()
    }
}

let handler = DataHandler(modelContainer: container)
try await handler.importTrips(records)

ルール:ModelContainer は Sendable です。ModelContext は Sendable ではありません。生成元のアクターで使用してください。境界をまたいで渡すのは PersistentIdentifier(Sendable)です。@Model オブジェクトをアクター間で渡してはいけません。

SwiftUI との統合

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
            .modelContainer(for: [Trip.self, LivingAccommodation.self])
    }
}

struct DetailView: View {
    @Environment(\.modelContext) private var modelContext
    let trip: Trip
    var body: some View {
        Text(trip.name)
        Button("Delete") { modelContext.delete(trip) }
    }
}

#Preview {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(for: Trip.self, configurations: config)
    container.mainContext.insert(Trip(name: "Preview", destination: "London",
        startDate: .now, endDate: .now + 86400))
    return TripListView().modelContainer(container)
}

よくある間違い

1. struct に @Model を付ける -- class を使用してください。@Model には参照セマンティクスが必要です。

2. デフォルト値を指定せずに @Transient を使う -- 必ずデフォルト値を指定してください:@Transient var x: Bool = false。

3. .modelContainer がない -- ビュー階層にコンテナーがないと、@Query は空の結果を返します。

4. モデルオブジェクトをアクター間で渡す:

// WRONG: await handler.process(trip: trip)
// CORRECT: await handler.process(tripID: trip.persistentModelID)

5. 間違ったアクターで ModelContext を使う:

// WRONG: Task.detached { context.fetch(...) }
// CORRECT: Use @ModelActor for background work

6. サポートされていない #Predicate の式:

// WRONG: #Predicate<Trip> { $0.name.uppercased() == "PARIS" }
// CORRECT: #Predicate<Trip> { $0.name.localizedStandardContains("paris") }

7. #Predicate 内のフロー制御:

// WRONG: #Predicate<Trip> { for tag in $0.tags { ... } }
// CORRECT: #Predicate<Trip> { $0.tags.contains { $0.name == "x" } }

8. @ModelActor で保存しない -- 必ず try modelContext.save() を明示的に呼び出してください。

9. @Model で ObservableObject を使う -- ObservableObject/@Published は使用しないでください。@Model は Observable を生成します。ビューでは @Query を使用してください。

10. デフォルト値のない非オプショナルなリレーションシップ:

// WRONG: var accommodation: LivingAccommodation  // crashes on reconstitution
// CORRECT: var accommodation: LivingAccommodation?

11. 逆リレーションシップのないカスケード -- カスケード削除を確実に機能させるため、inverse: を指定してください。

12. バックグラウンドのデータ処理に DispatchQueue を使う:

// WRONG: DispatchQueue.global().async { ModelContext(container).fetch(...) }
// CORRECT: @ModelActor actor Handler { func fetch() throws { ... } }

レビューチェックリスト

  • すべての @Model が、指定イニシャライザーを持つクラスである
  • すべての @Transient プロパティにデフォルト値がある
  • リレーションシップで deleteRule と inverse を指定している
  • シーンまたはルートビューのレベルに .modelContainer を設定している
  • SwiftUI でのリアクティブなデータ表示に @Query を使用している
  • #Predicate でサポートされている演算子のみを使用している
  • バックグラウンド処理に @ModelActor を使用している
  • アクター境界をまたぐ箇所で PersistentIdentifier を使用している
  • スキーマの変更に VersionedSchema と SchemaMigrationPlan がある
  • 大きなデータに @Attribute(.externalStorage) を使用している
  • CloudKit モデルで一意性制約を避け、リレーションシップをオプショナルにし、.deny を避け、スカラー値を一律にオプショナルにしていない
  • CloudKit 同期で iCloud と CloudKit、リモート通知、本番環境へのスキーマ展開を確認している
  • @ModelActor のメソッドで save() を明示的に呼び出している
  • プレビューで ModelConfiguration(isStoredInMemoryOnly: true) を使用している
  • SwiftUI ビューからアクセスする @Model クラスが、@ModelActor または MainActor の隔離によって @MainActor 上にある

リファレンス

コメント