Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
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.
For teams that haven't adopted SwiftData, Core Data remains a fully supported persistence framework.
Docs: NSPersistentContainer, Setting up a Core Data stack
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()
}
}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()
}
}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)
}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)
}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 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
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
)Key rules for coexistence:
@Model class name must match the Core Data entity name.@Attribute(originalName:) if you renamed properties.// 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
}
}// 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 .xcdatamodeldautomaticallyMergesChangesFromParent and
NSPersistentStoreRemoteChangeNotification help detect changes from the
other stack..xcdatamodeld and
@Model cause crashes.@Model ClassesCreate 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
}
}| Core Data Type | SwiftData Type |
|---|---|
| String | String |
| Boolean | Bool |
| Integer 16/32/64 | Int |
| Float / Double | Float / Double |
| Date | Date |
| Binary Data | Data |
| UUID | UUID |
| URI | URL |
| Decimal | Decimal |
| Transformable | Codable struct (composite, iOS 18+) |
| To-one relationship | Optional reference to @Model |
| To-many relationship | Array of @Model |
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)]
}
}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)
}
}@Model class with identical property names and typesVersionedSchema and SchemaMigrationPlan defined for non-trivial schema changesModelConfiguration points to the existing Core Data SQLite fileautomaticallyMergesChangesFromParent enabled on Core Data's viewContext.xcdatamodeld removed only after full migration is verifiedskills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references