A lightweight, production-ready, thread-safe Core Data manager and singleton for iOS, macOS, tvOS, watchOS, and visionOS.
Zero third-party dependencies. Full CRUD, generics, batch operations, async/await, Combine, in-memory testing, and persistent history tracking.
- Features
- Requirements
- Installation
- Quick Start
- Architecture & Context Management
- API Reference
- Unit Testing & SwiftUI Previews
- Swift 6 & Strict Concurrency
- Contributing
- License
- 🚀 Zero Dependencies: Pure Swift & Apple CoreData framework.
- 🔒 Swift 6 & Strict Concurrency Ready: Fully compliant with
-strict-concurrency=completeand@MainActorthread confinement. - ⚡ Dual-Tier Context Architecture: Main-queue
viewContextfor UI and private-queue background contexts for heavy operations. - 🧬 Generics First: Strongly-typed APIs (
createObject,fetchObjects,updateObjects,deleteAllObjects,upsertObject). - 🏎️ Fast Batch Operations: Store-level SQLite
batchUpdateandbatchDeletewith automatic UIviewContextchange-merging. - 🔄 Upsert Support: Effortless find-or-create patterns in a single call.
- 🌊 Modern Async/Await: Full async support for background queries and tasks (
fetchAsync,saveAsync,performAndSaveAsync). - 📡 Reactive Combine: Scope-filtered
storeChangedPublishernotifying when your stores are saved. - 🧪 Test-Ready: First-class support for isolated in-memory stores (
CoreDataManager.inMemory()) for unit tests and SwiftUI previews. - 🌐 Cross-Process & Remote Sync: Automatic Persistent History Tracking for App Extensions and multi-process architectures with built-in history pruning.
- 🩺 Deep Validation Diagnostics: Automatically decodes and logs
NSDetailedErrorsKeywhen saves fail.
| Platform | Minimum Deployment Target |
|---|---|
| iOS | 14.0+ (async/await APIs on 15.0+) |
| macOS | 11.0+ (async/await APIs on 12.0+) |
| tvOS | 14.0+ (async/await APIs on 15.0+) |
| watchOS | 7.0+ (async/await APIs on 8.0+) |
| visionOS | 1.0+ |
| Swift | 5.10+ (Fully validated in Swift 6.0+) |
| Xcode | 15.3+ (Xcode 16+ for Swift 6 mode) |
- In Xcode, select File > Add Package Dependencies...
- Enter the repository URL:
https://github.com/bhargavkukadiya/CoreDataManager.git - Choose the dependency rule (e.g. Up to Next Major Version) and click Add Package.
Add CoreDataManager to your package dependencies in Package.swift:
dependencies: [
.package(url: "https://github.com/bhargavkukadiya/CoreDataManager.git", from: "1.0.0")
]And add CoreDataManager to your target dependencies:
.target(
name: "MyApp",
dependencies: ["CoreDataManager"]
)Alternatively, simply copy Sources/CoreDataManager/CoreDataManager.swift directly into your project.
import CoreData
import CoreDataManager
let manager = CoreDataManager.shared
// 1. Create
manager.create(entityName: "User") { obj in
obj.setValue(UUID(), forKey: "id")
obj.setValue("Bhargav", forKey: "name")
}
manager.save()
// 2. Fetch
let users = manager.fetch("User")
// 3. Update
manager.update("User",
predicate: NSPredicate(format: "name == %@", "Bhargav")) { obj in
obj.setValue("Bhargav S.", forKey: "name")
}
// 4. Delete
manager.deleteAll("User",
predicate: NSPredicate(format: "name == %@", "Bhargav S."))┌────────────────────────────────────────────────────────┐
│ CoreDataManager │
└──────────────────────────┬─────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ viewContext │ │ Background Ctx │
│ @MainActor │ │ Private Queue │
│ (UI Reads / │ │ (Bulk / Imports │
│ Light Writes)│ │ Store Batches) │
└──────┬───────┘ └────────┬────────┘
│ │
│ Automatic Merging │
├───────────────────────────────┤
▼ ▼
┌────────────────────────────────────────────────────────┐
│ NSPersistentStoreCoordinator │
└──────────────────────────┬─────────────────────────────┘
▼
┌────────────────────────────────────────────────────────┐
│ SQLite Data Store │
└────────────────────────────────────────────────────────┘
viewContext: Runs on the main queue (@MainActor). Automatically merges changes saved from background contexts.newBackgroundContext(): Returns a private-queue context withNSMergeByPropertyObjectTrumpMergePolicy.performBackgroundTask: Executes work on an isolated background context and persists changes automatically.
// Save viewContext on the MainActor
let success: Bool = manager.save()
try manager.saveThrowing()
// Save a specific background context
manager.save(context: backgroundCtx)
try manager.saveThrowing(context: backgroundCtx)
// Execute in background and save automatically
manager.performBackgroundTask({ ctx in
// Background work on ctx
}) { success in
print("Background save complete:", success)
}// Untyped creation
let obj = manager.create(entityName: "User") { obj in
obj.setValue("Alice", forKey: "name")
}
// Strongly-typed generic creation (MainActor)
let user = manager.createObject(of: User.self) { u in
u.id = UUID()
u.name = "Alice"
u.age = 28
}
// In a specific background context
let bgUser = manager.createObject(of: User.self, context: backgroundCtx) { u in
u.name = "Bob"
}// Untyped fetch
let allUsers = manager.fetch("User")
let filtered = manager.fetch("User",
predicate: NSPredicate(format: "age >= %d", 18),
sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
fetchLimit: 50)
// Strongly-typed fetch
let users: [User] = manager.fetchObjects(
of: User.self,
predicate: NSPredicate(format: "isActive == true"),
sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false)]
)
// Fetch by NSManagedObjectID
let user = manager.fetchObject(withID: objectID)
// Fast Count & Exists (executed at store-level, zero object faults)
let count = manager.countObjects(of: User.self)
let exists = manager.existsObject(of: User.self, predicate: NSPredicate(format: "name == %@", "Alice"))// In-memory update on viewContext
manager.updateObjects(of: User.self,
predicate: NSPredicate(format: "isActive == false")) { user in
user.isActive = true
}
// Store-level Batch Update (ultra-fast for thousands of rows)
// Bypasses memory faults and automatically merges changes into viewContext
let updatedRows = manager.batchUpdate(
"User",
propertiesToUpdate: ["isActive": true],
predicate: NSPredicate(format: "lastSeen < %@", cutoffDate as CVarArg)
)
// Throwing variant
try manager.batchUpdateThrowing("User", propertiesToUpdate: ["isActive": true])// Single object deletion (MainActor)
manager.delete(user)
// Single object deletion in a specific context
manager.delete(user, in: backgroundCtx)
// Bulk in-memory deletion
manager.deleteAllObjects(of: User.self)
manager.deleteAllObjects(of: User.self, predicate: NSPredicate(format: "isDeleted == true"))
// Store-level Batch Delete (ultra-fast for large datasets)
// Deletes directly in SQLite and automatically merges tombstones into viewContext
manager.batchDelete("User", predicate: NSPredicate(format: "createdAt < %@", cutoffDate as CVarArg))
try manager.batchDeleteThrowing("User")Insert a new record or update an existing record in a single atomic call:
let user = manager.upsertObject(
of: User.self,
predicate: NSPredicate(format: "id == %@", userId as CVarArg)
) { user in
user.id = userId
user.name = "Bhargav"
user.lastLogin = Date()
}// Save asynchronously
try await manager.saveAsync()
// Perform background work and save asynchronously
try await manager.performAndSaveAsync { ctx in
let user = NSEntityDescription.insertNewObject(forEntityName: "User", into: ctx)
user.setValue("Async User", forKey: "name")
}
// Fetch in background, returning MainActor-confined objects safely
let users = await manager.fetchAsync(
of: User.self,
predicate: NSPredicate(format: "isActive == true"),
sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)]
)Generate configured NSFetchedResultsController instances for UITableView, UICollectionView, or SwiftUI bindings:
let frc: NSFetchedResultsController<User> = manager.makeTypedFetchedResultsController(
of: User.self,
predicate: NSPredicate(format: "isActive == true"),
sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
sectionNameKeyPath: nil,
cacheName: "UserListCache"
)
frc.delegate = self
try frc.performFetch()Subscribe to save events scoped strictly to your store coordinator:
manager.storeChangedPublisher
.sink { [weak self] in
self?.tableView.reloadData()
}
.store(in: &cancellables)// Discard unsaved changes in viewContext
manager.resetContext()
// Irreversibly wipe and rebuild SQLite store (e.g. on user logout / account wipe)
manager.destroyAndRebuildStore()
// Prune persistent history change logs older than 7 days to keep SQLite compact
manager.purgeHistory()
manager.purgeHistory(before: Date().addingTimeInterval(-30 * 24 * 3600))CoreDataManager provides first-class support for ephemeral in-memory stores that run purely in RAM and never touch disk:
import XCTest
import CoreData
@testable import MyApp
final class UserTests: XCTestCase {
var manager: CoreDataManager!
override func setUp() {
super.setUp()
// Creates an isolated, in-memory store instance
manager = CoreDataManager.inMemory(modelName: "Model", bundle: Bundle.main)
}
override func tearDown() {
manager = nil
super.tearDown()
}
@MainActor
func testCreateUser() {
let user = manager.createObject(of: User.self) { u in
u.name = "Alice"
}
XCTAssertNotNil(user)
XCTAssertTrue(manager.save())
XCTAssertEqual(manager.countObjects(of: User.self), 1)
}
}CoreDataManager is built for Swift 6 strict concurrency:
@unchecked Sendable: Configuration state is immutable after initialization (modelName,persistentContainer,inMemory).@MainActorConfinement: Methods that interact directly withviewContextare marked@MainActor, eliminating data races at compile time.- Thread Confinement Rule:
NSManagedObjectinstances are not Sendable. Always passNSManagedObjectIDacross threads/actors and re-fetch viacontext.existingObject(with:).
Contributions, feature requests, and issue reports are welcome!
- Fork the Project.
- Create your Feature Branch (
git checkout -b feature/AmazingFeature). - Commit your Changes (
git commit -m 'Add some AmazingFeature'). - Push to the Branch (
git push origin feature/AmazingFeature). - Open a Pull Request.
Distributed under the MIT License. See LICENSE for more information.