Skip to content

A library aimed at modernizing Core Data by incorporating the elegance and safety of SwiftData-style concurrency.

License

Notifications You must be signed in to change notification settings

fatbobman/CoreDataEvolution

Repository files navigation

CoreDataEvolution

Swift 6 iOS macOS watchOS visionOS tvOS License: MIT Ask DeepWiki

Revolutionizing Core Data with SwiftData-inspired Concurrent Operations

Welcome to CoreDataEvolution, a library aimed at modernizing Core Data by incorporating the elegance and safety of SwiftData-style concurrency. This library is designed to simplify and enhance Core Data’s handling of multithreading, drawing inspiration from SwiftData's @ModelActor feature, enabling efficient, safe, and scalable operations.


Don't miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to Fatbobman's Swift Weekly and receive weekly insights and valuable content directly to your inbox.


Motivation

SwiftData introduced modern concurrency features like @ModelActor, making it easier to handle concurrent data access with safety guaranteed by the compiler. However, SwiftData's platform requirements and limited maturity in certain areas have deterred many developers from adopting it. CoreDataEvolution bridges the gap, bringing SwiftData’s advanced design into the Core Data world for developers who are still reliant on Core Data.

Key Features

  • Custom Executors for Core Data Actors
    CoreDataEvolution provides custom executors that ensure all operations on managed objects are performed on the appropriate thread associated with their managed object context. It uses a UnownedJob-based serial executor path compatible with the minimum supported OS versions.

  • @NSModelActor Macro
    The @NSModelActor macro simplifies Core Data concurrency, mirroring SwiftData’s @ModelActor macro. It generates the necessary boilerplate code to manage a Core Data stack within an actor, ensuring safe and efficient access to managed objects.

  • NSMainModelActor Macro NSMainModelActor is the main-thread companion macro for classes. It binds modelContext to viewContext and provides the same convenience access APIs (subscript, withContext) through NSMainModelActor protocol extensions.

  • Elegant Actor-based Concurrency
    CoreDataEvolution allows you to create actors with custom executors tied to Core Data contexts, ensuring that all operations within the actor are executed serially on the context’s thread.

Example Usage

Here’s how you can use CoreDataEvolution to manage concurrent Core Data operations with an actor:

import CoreDataEvolution

@NSModelActor
actor DataHandler {
    func updateItem(identifier: NSManagedObjectID, timestamp: Date) throws {
        guard let item = self[identifier, as: Item.self] else {
            throw MyError.objectNotExist
        }
        item.timestamp = timestamp
        try modelContext.save()
    }
}

In this example, the @NSModelActor macro simplifies the setup, automatically creating the required executor and Core Data stack inside the actor. Developers can then focus on their business logic without worrying about concurrency pitfalls.

This approach allows you to safely integrate modern Swift concurrency mechanisms into your existing Core Data stack, enhancing performance and code clarity.

You can disable the automatic generation of the constructor by using disableGenerateInit:

@NSModelActor(disableGenerateInit: true)
public actor DataHandler {
    let viewName: String

    func createNemItem(_ timestamp: Date = .now, showThread: Bool = false) throws -> NSManagedObjectID {
        let item = Item(context: modelContext)
        item.timestamp = timestamp
        try modelContext.save()
        return item.objectID
    }

    init(container: NSPersistentContainer, viewName: String) {
        modelContainer = container
        self.viewName = viewName
        let context = container.newBackgroundContext()
        context.name = viewName
        modelExecutor = .init(context: context)
    }
}

NSMainModelActor is the main-thread companion macro for classes:

@MainActor
@NSMainModelActor
final class DataHandler {
    func updateItem(identifier: NSManagedObjectID, timestamp: Date) throws {
        guard let item = self[identifier, as: Item.self] else {
            throw MyError.objectNotExist
        }
        item.timestamp = timestamp
        try modelContext.save()
    }
}

Protocol APIs

Types decorated with @NSModelActor and @NSMainModelActor gain these APIs through their corresponding protocol extensions (NSModelActor and NSMainModelActor):

Properties

Property Description
modelContext: NSManagedObjectContext The managed object context associated with this actor. All Core Data operations should go through this context.
modelContainer: NSPersistentContainer The persistent container that owns this actor's context.

Subscript

Retrieve a managed object by its NSManagedObjectID, cast to the expected type. Returns nil if the object does not exist or the cast fails.

// Inside an actor method
guard let item = self[objectID, as: Item.self] else {
    throw MyError.objectNotFound
}
item.timestamp = .now
try modelContext.save()

withContext

Provides direct, synchronous access to the actor's context from within the actor's isolation. The closure runs synchronously with no additional scheduling overhead.

This method is primarily intended for unit tests — use it to inspect the persistent store state after a write operation, without going through the actor's higher-level API.

// Verify state after a write — single-context overload
try await handler.withContext { context in
    let request = Item.fetchRequest()
    let items = try context.fetch(request)
    #expect(items.count == 1)
}

An overload also provides the NSPersistentContainer when needed: withContext { context, container in ... }.

Note: For production writes, prefer the actor's dedicated mutation methods so that save/rollback logic remains consistent.

Testing Utilities

NSPersistentContainer.makeTest

Creates an isolated, on-disk SQLite store for each test, avoiding the two most common pitfalls:

  • /dev/null (shared in-memory): all tests sharing the same URL read from and write to the same store — parallel execution causes data leakage and deadlocks.
  • Named in-memory stores: WAL sidecar files (.sqlite-shm, .sqlite-wal) can linger between runs, producing phantom data.

makeTest solves this by using #fileID-#function as the default testName, so each test call site automatically gets its own store file. Stale files from the previous run are deleted before the store loads.

@Test func createItem() async throws {
    // testName defaults to #fileID-#function — each test call site gets its own store
    let container = NSPersistentContainer.makeTest(model: MySchema.objectModel)
    let handler = DataHandler(container: container)
    // … test body …
}

Parameters:

Parameter Type Default Description
model NSManagedObjectModel The managed object model for your schema.
testName String "" Optional explicit store name. When empty, the name is derived from call-site #fileID-#function.
fileID String #fileID Call-site file identity used when testName is empty.
function String #function Call-site function identity used when testName is empty.
subDirectory String "CoreDataEvolutionTestTemp" Temp sub-directory that holds the SQLite files.

Note: Store files are not deleted immediately after a test completes — they are cleaned up at the start of the next run with the same testName, so you can inspect them for debugging if needed.

Installation

You can add CoreDataEvolution to your project using Swift Package Manager by adding the following dependency to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/fatbobman/CoreDataEvolution.git", .upToNextMajor(from: "0.7.3"))
]

Then, import the module into your Swift files:

import CoreDataEvolution

System Requirements

  • iOS 13.0+ / macOS 10.15+ / watchOS 6.0+ / visionOS 1.0+ / tvOS 13.0+
  • Swift 6.0

Note: The custom executor uses a compatible UnownedJob serial-executor path to support the minimum deployment targets.

Contributing

We welcome contributions! Whether you want to report issues, propose new features, or contribute to the code, feel free to open issues or pull requests on the GitHub repository.

License

CoreDataEvolution is available under the MIT license. See the LICENSE file for more information.

Acknowledgments

Special thanks to the Swift community for their continuous support and contributions. Thanks to @rnine for sharing and validating the iOS 13+ compatibility approach that inspired this adaptation.

Support the project

Star History

Star History Chart

About

A library aimed at modernizing Core Data by incorporating the elegance and safety of SwiftData-style concurrency.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors