CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

core-data-coexistence.mdskills/swiftdata/references/

Core Data Coexistence

Standalone Core Data patterns for projects not yet on SwiftData, strategies for running Core Data and SwiftData side by side against the same store, and migration guidance for transitioning from Core Data to SwiftData.

Contents

  • Standalone Core Data Stack
  • Core Data + SwiftData Coexistence
  • Migration from Core Data to SwiftData

Standalone Core Data Stack

For teams that haven't adopted SwiftData, Core Data remains a fully supported persistence framework.

Docs: NSPersistentContainer, Setting up a Core Data stack

NSPersistentContainer Setup

NSPersistentContainer encapsulates the Core Data stack: the managed object model, the persistent store coordinator, and the managed object context.

import CoreData

final class CoreDataStack: @unchecked Sendable {
    static let shared = CoreDataStack()

    let container: NSPersistentContainer

    private init() {
        // Name must match the .xcdatamodeld file name
        container = NSPersistentContainer(name: "MyAppModel")

        container.loadPersistentStores { description, error in
            if let error {
                fatalError("Core Data store failed to load: \(error)")
            }
        }

        // Automatically merge changes from background contexts
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }

    /// The main-thread context for UI reads
    var viewContext: NSManagedObjectContext {
        container.viewContext
    }

    /// A new background context for writes
    func newBackgroundContext() -> NSManagedObjectContext {
        container.newBackgroundContext()
    }
}

NSManagedObjectContext Usage

The viewContext is bound to the main queue; use it for reads and UI. Use background contexts for writes and batch operations.

// Reading on the main context
func fetchTrips() throws -> [CDTrip] {
    let context = CoreDataStack.shared.viewContext
    let request = CDTrip.fetchRequest()
    request.sortDescriptors = [NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: true)]
    return try context.fetch(request)
}

// Writing on a background context
func createTrip(name: String, destination: String) async throws {
    let context = CoreDataStack.shared.newBackgroundContext()
    try await context.perform {
        let trip = CDTrip(context: context)
        trip.name = name
        trip.destination = destination
        trip.startDate = Date.now
        trip.id = UUID()
        try context.save()
    }
}

NSFetchRequest with NSPredicate and NSSortDescriptor

import CoreData

func fetchUpcomingTrips(destination: String) throws -> [CDTrip] {
    let context = CoreDataStack.shared.viewContext
    let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()

    // Predicate: filter by destination and future start date
    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
        NSPredicate(format: "destination ==[cd] %@", destination),
        NSPredicate(format: "startDate > %@", Date.now as NSDate)
    ])

    // Sort: newest first
    request.sortDescriptors = [
        NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: false)
    ]

    // Performance: limit results and prefetch relationships
    request.fetchLimit = 20
    request.relationshipKeyPathsForPrefetching = ["accommodation"]

    return try context.fetch(request)
}

// Counting without loading objects
func countFavoriteTrips() throws -> Int {
    let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
    request.predicate = NSPredicate(format: "isFavorite == YES")
    return try CoreDataStack.shared.viewContext.count(for: request)
}

Saving Context and Error Handling

func saveContext(_ context: NSManagedObjectContext) throws {
    guard context.hasChanges else { return }

    do {
        try context.save()
    } catch {
        // Roll back unsaved changes to prevent inconsistent state
        context.rollback()

        if let nsError = error as NSError? {
            // Check for common Core Data errors
            switch nsError.code {
            case NSManagedObjectConstraintMergeError:
                // Unique constraint violation -- handle merge conflict
                throw PersistenceError.constraintViolation(nsError)
            case NSValidationMissingMandatoryPropertyError:
                // Required property is nil
                throw PersistenceError.validationFailed(nsError)
            default:
                throw PersistenceError.saveFailed(nsError)
            }
        }
        throw error
    }
}

enum PersistenceError: Error {
    case constraintViolation(NSError)
    case validationFailed(NSError)
    case saveFailed(NSError)
}

Background Processing Pattern

func importTrips(_ records: [TripRecord]) async throws {
    let context = CoreDataStack.shared.newBackgroundContext()
    try await context.perform {
        // Batch insert for performance (iOS 13+)
        let batchInsert = NSBatchInsertRequest(
            entity: CDTrip.entity(),
            objects: records.map { record in
                [
                    "id": record.id,
                    "name": record.name,
                    "destination": record.destination,
                    "startDate": record.startDate
                ] as [String: Any]
            }
        )
        batchInsert.resultType = .count
        let result = try context.execute(batchInsert) as? NSBatchInsertResult
        print("Inserted \(result?.result as? Int ?? 0) trips")

        // Merge changes into viewContext
        NSManagedObjectContext.mergeChanges(
            fromRemoteContextSave: [NSInsertedObjectsKey: []],
            into: [CoreDataStack.shared.viewContext]
        )
    }
}

Core Data + SwiftData Coexistence

Core Data and SwiftData can share the same underlying SQLite store. This enables gradual migration: keep existing Core Data code running while introducing SwiftData for new features.

Docs: Adopting SwiftData for a Core Data app

Using the Same Underlying Store

Both stacks must point to the same SQLite file and agree on the schema. The Core Data .xcdatamodeld and SwiftData @Model classes must describe the same entities and properties.

import SwiftData
import CoreData

// 1. Determine the store URL that Core Data already uses
let storeURL = NSPersistentContainer.defaultDirectoryURL()
    .appendingPathComponent("MyAppModel.sqlite")

// 2. Point SwiftData at the same store
let config = ModelConfiguration(
    "MyAppModel",
    url: storeURL
)

let container = try ModelContainer(
    for: Trip.self,
    configurations: config
)

ModelConfiguration Pointing to Existing Core Data Store

Key rules for coexistence:

  1. The @Model class name must match the Core Data entity name.
  2. Property names and types must match exactly.
  3. Use @Attribute(originalName:) if you renamed properties.
  4. Both stacks should use the same store file.
// Core Data entity: CDTrip (entity name "Trip" in .xcdatamodeld)
// Attributes: name (String), destination (String), startDate (Date),
//             isFavorite (Boolean), imageData (Binary Data)

// Matching SwiftData model
@Model
class Trip {
    var name: String
    var destination: String
    var startDate: Date
    var isFavorite: Bool = false
    @Attribute(.externalStorage) var imageData: Data?

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

Gradual Coexistence Strategy

// Phase 1: Core Data stack still handles writes;
//          SwiftData reads the same store for new UI
@main
struct MyApp: App {
    let coreDataStack = CoreDataStack.shared  // Existing Core Data

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        // SwiftData reads from the same store
        .modelContainer(for: Trip.self, configurations:
            ModelConfiguration(url: coreDataStack.storeURL)
        )
    }
}

// Phase 2: New features use SwiftData for both reads and writes
// Phase 3: Migrate remaining Core Data code to SwiftData
// Phase 4: Remove Core Data stack and .xcdatamodeld

Important Coexistence Rules

  • Do not write to the same entity from both stacks simultaneously. Pick one stack per entity for writes to avoid conflicts.
  • Core Data's automaticallyMergesChangesFromParent and NSPersistentStoreRemoteChangeNotification help detect changes from the other stack.
  • Test thoroughly -- schema mismatches between the .xcdatamodeld and @Model cause crashes.

Migration from Core Data to SwiftData

Step 1: Map Core Data Entities to @Model Classes

Create a @Model class for each Core Data entity. Property names and types must align with the .xcdatamodeld definition.

// Core Data entity "Article"
// Attributes: id (UUID), title (String), body (String),
//             createdAt (Date), isDraft (Boolean)
// Relationships: author (to-one → Author), tags (to-many → Tag)

@Model
class Article {
    @Attribute(.unique) var id: UUID
    var title: String
    var body: String
    var createdAt: Date
    var isDraft: Bool = true

    @Relationship(deleteRule: .nullify, inverse: \Author.articles)
    var author: Author?

    @Relationship(deleteRule: .nullify, inverse: \Tag.articles)
    var tags: [Tag] = []

    init(id: UUID = UUID(), title: String, body: String, createdAt: Date = .now) {
        self.id = id
        self.title = title
        self.body = body
        self.createdAt = createdAt
    }
}

@Model
class Author {
    @Attribute(.unique) var id: UUID
    var name: String
    var articles: [Article] = []

    init(id: UUID = UUID(), name: String) {
        self.id = id
        self.name = name
    }
}

@Model
class Tag {
    @Attribute(.unique) var id: UUID
    var name: String
    var articles: [Article] = []

    init(id: UUID = UUID(), name: String) {
        self.id = id
        self.name = name
    }
}

Type Mapping Reference

Core Data TypeSwiftData Type
StringString
BooleanBool
Integer 16/32/64Int
Float / DoubleFloat / Double
DateDate
Binary DataData
UUIDUUID
URIURL
DecimalDecimal
TransformableCodable struct (composite, iOS 18+)
To-one relationshipOptional reference to @Model
To-many relationshipArray of @Model

Step 2: Schema Versioning Considerations

If the Core Data store has existing data, SwiftData must be able to open it. Use VersionedSchema and SchemaMigrationPlan for non-trivial changes.

// If the SwiftData model exactly matches the Core Data schema,
// no migration is needed -- SwiftData opens the store directly.

// For schema differences, define versioned schemas:
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Article.self, Author.self] }

    @Model class Article {
        var id: UUID
        var title: String
        var body: String
        var createdAt: Date
        init(id: UUID, title: String, body: String, createdAt: Date) {
            self.id = id; self.title = title
            self.body = body; self.createdAt = createdAt
        }
    }

    @Model class Author {
        var id: UUID
        var name: String
        init(id: UUID, name: String) { self.id = id; self.name = name }
    }
}

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

    @Model class Article {
        var id: UUID
        var title: String
        var body: String
        var createdAt: Date
        var isDraft: Bool = true  // New property
        init(id: UUID, title: String, body: String, createdAt: Date) {
            self.id = id; self.title = title
            self.body = body; self.createdAt = createdAt
        }
    }

    @Model class Author {
        var id: UUID
        var name: String
        init(id: UUID, name: String) { self.id = id; self.name = name }
    }

    @Model class Tag {
        var id: UUID
        var name: String
        init(id: UUID, name: String) { self.id = id; self.name = name }
    }
}

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

Step 3: Testing Migration Paths

Always test migration with real production data copies before shipping.

import XCTest
import SwiftData

final class MigrationTests: XCTestCase {

    func testCoreDataToSwiftDataMigration() throws {
        // 1. Copy a known Core Data store into the test bundle
        let sourceURL = Bundle(for: type(of: self))
            .url(forResource: "TestStore", withExtension: "sqlite")!

        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)

        let destURL = tempDir.appendingPathComponent("TestStore.sqlite")
        try FileManager.default.copyItem(at: sourceURL, to: destURL)

        // Copy WAL and SHM if they exist
        for ext in ["-wal", "-shm"] {
            let src = sourceURL.deletingLastPathComponent()
                .appendingPathComponent("TestStore.sqlite\(ext)")
            if FileManager.default.fileExists(atPath: src.path) {
                try FileManager.default.copyItem(
                    at: src,
                    to: tempDir.appendingPathComponent("TestStore.sqlite\(ext)")
                )
            }
        }

        // 2. Open with SwiftData
        let config = ModelConfiguration(url: destURL)
        let container = try ModelContainer(
            for: SchemaV2.Article.self,
            migrationPlan: ArticleMigrationPlan.self,
            configurations: config
        )

        // 3. Verify data survived migration
        let context = ModelContext(container)
        let articles = try context.fetch(FetchDescriptor<SchemaV2.Article>())
        XCTAssertFalse(articles.isEmpty, "Migration should preserve existing articles")

        // 4. Verify new properties have defaults
        for article in articles {
            XCTAssertTrue(article.isDraft, "New isDraft property should default to true")
        }

        // Cleanup
        try FileManager.default.removeItem(at: tempDir)
    }
}

Migration Checklist

  • Every Core Data entity has a matching @Model class with identical property names and types
  • Relationship inverse properties are specified in both directions
  • VersionedSchema and SchemaMigrationPlan defined for non-trivial schema changes
  • ModelConfiguration points to the existing Core Data SQLite file
  • Tested migration with a copy of production data
  • Only one stack writes to each entity during coexistence
  • automaticallyMergesChangesFromParent enabled on Core Data's viewContext
  • .xcdatamodeld removed only after full migration is verified

skills

CHANGELOG.md

README.md

tile.json