Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CoreDataManager

A lightweight, production-ready, thread-safe Core Data manager and singleton for iOS, macOS, tvOS, watchOS, and visionOS.

Swift Platforms SwiftPM Concurrency License

Zero third-party dependencies. Full CRUD, generics, batch operations, async/await, Combine, in-memory testing, and persistent history tracking.


Table of Contents


Features

  • 🚀 Zero Dependencies: Pure Swift & Apple CoreData framework.
  • 🔒 Swift 6 & Strict Concurrency Ready: Fully compliant with -strict-concurrency=complete and @MainActor thread confinement.
  • Dual-Tier Context Architecture: Main-queue viewContext for 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 batchUpdate and batchDelete with automatic UI viewContext change-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 storeChangedPublisher notifying 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 NSDetailedErrorsKey when saves fail.

Requirements

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)

Installation

Swift Package Manager (Xcode)

  1. In Xcode, select File > Add Package Dependencies...
  2. Enter the repository URL:
    https://github.com/bhargavkukadiya/CoreDataManager.git
    
  3. Choose the dependency rule (e.g. Up to Next Major Version) and click Add Package.

Package.swift

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"]
)

Direct Drop-in

Alternatively, simply copy Sources/CoreDataManager/CoreDataManager.swift directly into your project.


Quick Start

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."))

Architecture & Context Management

┌────────────────────────────────────────────────────────┐
│                   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 with NSMergeByPropertyObjectTrumpMergePolicy.
  • performBackgroundTask: Executes work on an isolated background context and persists changes automatically.

API Reference

Save Operations

// 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)
}

Create Operations

// 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"
}

Fetch Operations

// 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"))

Update Operations

// 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])

Delete Operations

// 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")

Upsert (Find-or-Create)

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()
}

Async / Await (iOS 15+)

// 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)]
)

NSFetchedResultsController

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()

Combine Publisher

Subscribe to save events scoped strictly to your store coordinator:

manager.storeChangedPublisher
    .sink { [weak self] in
        self?.tableView.reloadData()
    }
    .store(in: &cancellables)

Store Maintenance & History Pruning

// 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))

Unit Testing & SwiftUI Previews

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)
    }
}

Swift 6 & Strict Concurrency

CoreDataManager is built for Swift 6 strict concurrency:

  • @unchecked Sendable: Configuration state is immutable after initialization (modelName, persistentContainer, inMemory).
  • @MainActor Confinement: Methods that interact directly with viewContext are marked @MainActor, eliminating data races at compile time.
  • Thread Confinement Rule: NSManagedObject instances are not Sendable. Always pass NSManagedObjectID across threads/actors and re-fetch via context.existingObject(with:).

Contributing

Contributions, feature requests, and issue reports are welcome!

  1. Fork the Project.
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature).
  3. Commit your Changes (git commit -m 'Add some AmazingFeature').
  4. Push to the Branch (git push origin feature/AmazingFeature).
  5. Open a Pull Request.

License

Distributed under the MIT License. See LICENSE for more information.


Made with ❤️ by Bhargav Kukadiya

About

A production-ready, thread-safe Core Data manager for iOS, macOS, tvOS, watchOS, and visionOS. Features full CRUD, generics, batch operations, Combine, async/await, in-memory testing, and Swift 6 strict concurrency.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages