{"id":32146831,"url":"https://github.com/rccoop/rcdatakit","last_synced_at":"2026-02-18T22:01:29.506Z","repository":{"id":259282417,"uuid":"840818114","full_name":"RCCoop/RCDataKit","owner":"RCCoop","description":"Helpers for Core Data","archived":false,"fork":false,"pushed_at":"2025-04-16T21:52:00.000Z","size":255,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-21T08:51:39.777Z","etag":null,"topics":["core-data","coredata","nsmanagedobjectmodel","nspersistentcontainer","nspredicate","nsstagedmigrationmanager","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RCCoop.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-10T19:20:55.000Z","updated_at":"2025-04-16T21:49:57.000Z","dependencies_parsed_at":"2024-10-24T05:53:43.753Z","dependency_job_id":null,"html_url":"https://github.com/RCCoop/RCDataKit","commit_stats":null,"previous_names":["rccoop/rcdatakit"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/RCCoop/RCDataKit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RCCoop%2FRCDataKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RCCoop%2FRCDataKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RCCoop%2FRCDataKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RCCoop%2FRCDataKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RCCoop","download_url":"https://codeload.github.com/RCCoop/RCDataKit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RCCoop%2FRCDataKit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29596329,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-18T20:59:56.587Z","status":"ssl_error","status_checked_at":"2026-02-18T20:58:41.434Z","response_time":162,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["core-data","coredata","nsmanagedobjectmodel","nspersistentcontainer","nspredicate","nsstagedmigrationmanager","swift"],"created_at":"2025-10-21T08:30:39.291Z","updated_at":"2026-02-18T22:01:29.500Z","avatar_url":"https://github.com/RCCoop.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🛠️ RCDataKit 💾\n\n![GitHub](https://img.shields.io/github/license/RCCoop/RCDataKit)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRCCoop%2FRCDataKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/RCCoop/RCDataKit)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRCCoop%2FRCDataKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/RCCoop/RCDataKit)\n\nHelpful tools for Core Data\n\n## Contents\n- [What Is It?](#what-is-it)\n- [Installation](#installation)\n- [Creating a Data Stack](#creating-a-data-stack)\n    - [DataStack Protocol](#datastack-protocol)\n        - [Default DataStack Implementations](#default-datastack-implementations)\n    - [Helper Types](#helper-types)\n        - [TransactionAuthor](#transactionauthor)\n        - [ModelManager and ModelFileManager Protocols](#modelmanager-and-modelfilemanager-protocols)\n        - [ModelVersion Protocol](#modelversion-protocol)\n        - [PersistentHistoryTracker actor](#persistenthistorytracker-actor)\n        - [SwiftUI Integration](#swiftui-integration)\n- [CRUD Helpers](#crud-helpers)\n    - [Typed NSManagedObjectID](#typed-nsmanagedobjectid)\n    - [Updatable Protocol](#updatable-protocol)\n    - [Persistable Protocol](#persistable-protocol)\n    - [NSManagedObjectContext Helpers](#nsmanagedobjectcontext-helpers)\n    - [NSFetchRequest Helpers](#nsfetchrequest-helpers)\n    - [NSPredicate Helpers](#nspredicate-helpers)\n- [Further Plans](#further-plans)\n- [Contribution/Feedback](#contributionfeedback)\n\n## What Is It?\n\nCore Data is a big and powerful tool, but it has never felt to me that it’s easy to learn. I’ve been using it heavily for years, and I still get confused when I try to do new things with it. **RCDataKit** is a collection of helper tools I’ve made over the years to make it all just a little easier.\n\n**RCDataKit** is a work in progress, and I’m intentionally keeping it fairly simple so that beginners can hopefully learn from it by browsing the source code. If you want something a lot more powerful, check out these really great libraries:\n\n- [JohnEstropia/CoreStore](https://github.com/JohnEstropia/CoreStore)\n- [ftchirou/PredicateKit](https://github.com/ftchirou/PredicateKit)\n- [jessesquires/JSQCoreDataKit](https://github.com/jessesquires/JSQCoreDataKit/tree/main)\n\n#### Why Not Just Use SwiftData?\n\nSure. Core Data is old and can be annoying to work with, and may be replaced permanently by SwiftData some day. But for now, I’m still using Core Data in my own projects because SwiftData just doesn’t work as well as I want it to. Until it’s got a lot of bugs worked out, I’ll keep working with the tried and true tool, even if it’s sometimes frustrating and difficult to learn.\n\n## Installation\n\n### Requirements\n\n- Minimum:\n    - iOS/tvOS/Catalyst: 15+\n    - macOS: 12+\n    - watchOS: 8+\n- Added Support for Staged Model Migrations:\n    - iOS/tvOS/Catalyst 17+\n    - macOS: 14+\n    - watchOS: 10+\n\n### Swift Package Manager\n\nIn your own Package, add the following to your dependencies:\n\n```swift\ndependencies: [\n  .package(url: \"https://github.com/RCCoop/RCDataKit\", .upToNextMajor(from: \"0.1.0\"))\n]\n```\n\nOr add the package to your Xcode project with `File -\u003e Add Package Dependencies...`\n\n# Creating a Data Stack\n\n## DataStack Protocol\nThis simple protocol is for types that wrap a `NSPersistentContainer` and  provide pre-configured `NSManagedObjectContexts`.\n\n```swift\nlet myStack: DataStack\n\n// Get the viewContext -- a NSManagedObjectContext where\n// transactionAuthor == myStack.mainContextAuthor.name\nlet viewContext = myStack.viewContext\n\n// Get a background context where transactionAuthor == TransactionAuthor.cloudDataImport.name\nlet bgContext = myStack.backgroundContext(author: .cloudDataImport)\n\n// TransactionAuthor can also be initialized with a String literal\nlet anotherContext = myStack.backgroundContext(author: \"otherContext\")\n```\n\n### Default DataStack Implementations\n\nThere are a few pre-made implementations of `DataStack` available here:\n\n- **BasicDataStack** is a SQLite-backed stack with a single store, and initialization options for Persistent History Tracking and Staged Migrations.\n- **PreviewStack** is an in-memory store for use in SwiftUI previews or other non-persisted environments.\n- **TestingStack** is stored in a temporary directory so you can use it in test cases. (I've found that in-memory stores during test cases can have unpredictable exceptions that file-backed storage doesn't. Also, I've found that running test cases in parallel with stacks using the same `NSManagedObjectModel` often throw exceptions, so it's best to run these tests serially rather than concurrently.)\n\n## Helper Types\n\n### TransactionAuthor\nA simple type to keep track of the different context authors in your Persistent Store. This does nothing by itself, but is used in `DataStack` and `PersistentHistoryTracker` standardize your author titles.\n\nAn easy way to set this up is to make an extension for `TransactionAuthor` to make a pre-set list of authors-- one for each main-thread context that accesses your data store (the app's view context, a widget's context, etc.), and as many named background contexts as you like to keep track of who or what is writing to your store.\n\n```swift\nextension TransactionAuthor {\n    static var iOSViewContext: TransactionAuthor { \"iOSViewContext\" }\n    static var extensionContext: TransactionAuthor { \"extensionContext\" }\n    static var networkSync: TransactionAuthor { \"networkSync\" }\n    static var localEditing: TransactionAuthor { \"localEditing\" }\n}\n```\n\nOr you can create names for your `TransactionAuthor`s at the call site by just using String literals:\n\n```swift\nlet someContext = myStack.backgroundContext(author: \"EditingTransaction\")\n```\n\n\n### ModelManager and ModelFileManager Protocols\n\nThese two paired protocols (mostly `ModelFileManager`, which inherits from `ModelManager`) are required by several other parts of this library to automate some of the boilerplate in creating a `NSManagedObjectModel`. The `ModelFileManager` represents the `.xcdatamodeld` file that most of us use to create our managed object model.\n\nDefining a `ModelFileManager` is simple:\n\n```swift\nenum TestModelFile: ModelFileManager {\n    static var bundle: Bundle {\n        .main // or use .module if your model file is in a separate module\n    }\n    \n    static var modelName: String {\n        \"TestModel\"\n    }\n    \n    static let model: NSManagedObjectModel = {\n        // Use one of RCDataKit's convenience methods to create the model:\n        NSManagedObjectModel.named(modelName, in: bundle)\n    }()\n}\n```\n\nThen you can use your `ModelFileManager` type in initializers for `BasicDataStack` and `PreviewStack`, and in your `ModelVersion` protocol implementation.\n\n### ModelVersion Protocol\n\nMigrating your Model from one version to the next can be a huge pain— Lightweight Migrations are easy enough, but Custom Migrations not so much. But now we have [Staged Migrations](https://developer.apple.com/videos/play/wwdc2022/10120/)! Unfortunately, [Apple’s documentation](https://developer.apple.com/documentation/coredata/staged_migrations) is lacking. Thanks to [Pol Piela](https://www.polpiella.dev/staged-migrations) and [FatBobMan](https://fatbobman.com/en/posts/what-s-new-in-core-data-in-wwdc23/) for picking up the slack.\n\nWith the `ModelVersion` protocol, setting up staged migrations takes a lot less boilerplate code, so you can focus on the important work of performing the migration.\n\n1. Make a type that conforms to the protocol, have it reference the names of your model versions, and give it a `ModelFileManager` type:\n\n\u003cimg width=\"160\" alt=\"Screenshot_2024-09-15_at_10 24 58_AM\" src=\"https://github.com/user-attachments/assets/9a42ca82-72c2-4c17-afa7-7bba80fa9f7a\"\u003e\n\n```swift\nenum Versions: String, ModelVersion {\n    // See `ModelFileManager` protocol:\n    typealias ModelFile = TestModelFile\n\n    // One case per version in your xcdatamodeld file:\n    case v1 = \"Model\"\n    case v2 = \"Model2\"\n    case v3 = \"Model3\"\n    case v4 = \"Model4\"\n}\n```\n\n2. Create an array of migration stages to walk through your version upgrades:\n\n```swift\nextension Versions {\n    static func migrationStages() -\u003e [NSMigrationStage] {\n        [\n            v1.migrationStage(\n                toStage: .v2,\n                label: \"Lightweight Migration: V1 to V2\"),\n            v2.migrationStage(\n                toStage: .v3,\n                label: \"Custom Migration: V2 to V3\",\n                preMigration: { context in\n                    // Do work before model is updated from v2 to v3\n                } postMigration: { context in\n                    // Do work after model is updated\n                }),\n            v3.migrationStage(\n                toStage: .v4,\n                label: \"Lightweight Migration: V3 to V4\")\n        ]\n    }\n}\n```\n\n3. When creating your `NSPersistentContainer`, add a `NSStagedMigrationManager` to the description options:\n\n```swift\nlet migrationManager = Versions.migrationManager()\ncontainer.persistentStoreDescriptions\n    .first?\n    .setOption(migrationManager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey)\n```\n\nAlternately, just pass your `ModelVersion` into the initializer for `BasicDataStack`:\n\n```swift\nlet stack = try BasicDataStack(\n                    versionKey: Versions.self,\n                    mainAuthor: .iOSViewContext)\n```\n\n### PersistentHistoryTracker actor\n\nPersistent History Tracking is well-documented, but can still be very confusing. `PersistentHistoryTracker` is an actor that attaches to your `NSPersistentContainer` in order to manage all that tracking for you. It borrows very heavily from tutorials and projects by [Antoine Van Der Lee](https://www.avanderlee.com/swift/persistent-history-tracking-core-data/) and [FatBobMan](https://fatbobman.com/en/posts/persistenthistorytracking/) (especially FatBobMan’s [PersistentHistoryTrackingKit](https://github.com/fatbobman/PersistentHistoryTrackingKit/tree/main), thank you!), with some added helpers based on `TransactionAuthor`.\n\nTo begin tracking:\n\n```swift\n// Before loading your persistent store, set persistent history options:\nlet storeDescription = myPersistentContainer.persistentStoreDescriptions[0]\nlet trueOption = true as NSNumber\nstoreDescription.setOption(trueOption, forKey: NSPersistentHistoryTrackingKey)\nstoreDescription.setOption(trueOption, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)\n\n// Add a PersistentHistoryTracker to your container\nself.tracker = PersistentHistoryTracker(\n    container: myPersistentContainer,\n    currentAuthor: .iOSViewContext)\n\n// Start or stop monitoring as needed.\ntracker.startMonitoring()\n```\n\nYou can also enable tracking in `BasicDataStack` by passing in an instance of `PersistentHistoryTrackingOptions` to the initializer:\n\n```swift\nlet stack = try BasicDataStack(\n                    versionKey: Versions.self,\n                    mainAuthor: .iOSViewContext,\n                    persistentHistoryOptions: .init())\n\nstack.historyTracker?.startMonitoring()\n```\n\n### SwiftUI Integration\n\nYou can add your `DataStack` and its `viewContext` to the SwiftUI environment in a single call:\n\n```swift\nstruct MyView: View {\n    var myStack: MyDataStack\n    \n    var body: some View {\n        SubView()\n            .dataStackEnvironment(myStack)\n    }\n}\n```\n\nThe `.dataStackEnvironment(_:)` call wraps `.environment(_:_:)` calls for both the DataStack and ManagedObjectContext, so you can access either environment value with the following property wrappers:\n\n```swift\nstruct SubView: View {\n    /// This is a `NSManagedObjectContext` accessed by `myStack.viewContext`\n    @Environment(\\.managedObjectContext) var context\n    \n    /// This is a `any DataStack` equal to `myStack` from `MyView`.\n    @EnvironmentDataStack var dataStack\n    \n    var body: some View { ... }\n}\n```\n\nYou must set the DataStack into a view's environment using the `dataStackEnvironment(_:)` call in order to use the `@EnvironmentDataStack` property wrapper, or it will cause a fatal error.\n\n# CRUD Helpers\n\n### Typed NSManagedObjectID\n\nUse `TypedObjectID` in place of `NSManagedObjectID` anywhere that you want to enforce type safety around the ID. Because both `NSManagedObjectID` and the `TypedObjectID` wrapper are `Sendable`, they are the best way to send references to `NSManagedObject` between contexts.\n```swift\nlet viewContextPerson = Person(...) // get a person on the ViewContext\nlet personId = TypedObjectID(viewContextPerson) // personId refers only to Person type\n\ntry backgroundContext.perform {\n    // get a reference to the same Person from storage, but safe for this context:\n    let backgroundContextPerson = try backgroundContext.existingObject(with: personId)\n}\n```\n\n### Updatable Protocol\n\nAdd the `Updatable` protocol to your Model types to get some free functions. Protocol conformance has no requirements except that the implementing type is a `NSManagedObject` subclass.\n```swift\nlet rc = Person(...)\n\nrc.update(\\.age, value: 15) // now I'm 15 years old!\nrc.updateIfAvailable(\\.age, value: nil) // still 15, not nil!\nrc.update(\\.age, value: 16, minimumChange: 2) // still 15, because I only want to age in 2-year increments.\n\nrc.add(\\.friend, relation: dan) // dan is now my friend\nrc.add(\\.friend, relation: nil) // nothing happens, because nobody's there.\nrc.remove(\\.friend, relation: dan) // dan's not my friend anymore.\n```\n\nWhy bother with these, rather than `rc.age = 15`, or `rc.friend = dan`? Because if I’m already 15, or if Dan is already my friend, using the `=` operator still causes the `NSManagedObject.hasChanges` flag to be set to `true`. I like making sure that if something didn’t change, I can believe `hasChanges`.\n\n### Persistable Protocol\n\nImporting lots of data into Core Data doesn’t need to be chaotic. Just implement the `Persistable` protocol in the data type that you want to import, and the protocol will walk you through some steps to make sure everything is nice and orderly.\n\n```swift\nstruct ImportablePerson {\n    var firstName: String\n    var lastName: String\n    var age: Int\n    var townID: Int\n}\n\nextension ImportablePerson: Persistable {\n    typealias ImporterData = [Int : Town]\n\n    // This function is called once per import operation to provide any extra\n    // necessary data for the import\n    static func generateImporterData(\n        objects: [Self], \n        context: NSManagedObjectContext\n    ) throws -\u003e ImporterData {\n        let townRefs = try context.getTownsWithIds(objects.map(\\.townID))\n        return townRefs.reduce(into: [:]) { $0[$1] = $1.id }\n    }\n    \n    // Then, for each item in the import operation, this function does the import:\n    func importIntoContext(\n        _ context: NSManagedObjectContext,\n        importerData: inout ImporterData\n    ) -\u003e PersistenceResult {\n        let persistedPerson = PersistentPerson(context: context)\n        persistedPerson.firstName = firstName\n        persistedPerson.lastName = lastName\n        persistedPerson.age = age\n        persistedPerson.town = importerData[townID]\n        return .insert(persistedPerson.objectID)\n    }\n}\n```\n\nTo use the importer functions, just use the handy function on `NSManagedObjectContext`:\n\n```swift\nlet arrayResults: [PersistenceResult] = try context\n    .importPersistableObjects(importablePeople)\n\n// results can also be a dictionary keyed to Identifiers.\nlet dictionaryResults: [ImportablePerson.ID : PersistenceResult] = try context\n    .importPersistableObjects(importablePeople)\n```\n\n### NSManagedObjectContext Helpers\n\nThere are some extra functions in an extension of `NSManagedObjectContext` help with basic operations:\n\n- Save changes, but only if changes exist in the context. If you update object properties with the `Updatable` protocol functions, this will save you unnecessary `save()` calls.\n\n```swift\ntry context.saveIfNeeded()\n```\n\n- Get typed `NSManagedObjects` from the context.\n\n```swift\nlet somePerson = try context.existing(Person.self, withID: personID)\nlet somePeople = try context.existing(Person.self, withIDs: [ID1, ID2, ID3])\n```\n\n- Remove all objects of a given type from the context (with an optional `NSPredicate` to only remove objects that match the given criteria).\n\n```swift\ntry context.removeInstances(of: Person.self, matching: someNSPredicate)\n```\n\n### NSFetchRequest Helpers\n\nA little syntactic sugar for using chaining functions to build your `NSFetchRequest`:\n\n```swift\nlet fetchRequest = NSFetchRequest\u003cPerson\u003e(entityName: \"Person\")\n    .sorted(sortDescriptors)\n    .where(somePredicate)\n```\n\nAnd for building `NSSortDescriptor`:\n\n```swift\nlet sorting: [NSSortDescriptor] = [\n    .ascending(\\Person.lastName),\n    .descending(\\Person.age)\n]\n```\n\n### NSPredicate Helpers\n\n`NSPredicate` can be combined with `\u0026\u0026`, `||`, and `!` operators. Collections of `NSPredicate` can be joined with `joined(with:)`.\n\n```swift\nlet predicate1 = NSPredicate(format: \"'age' \u003e= 13\")\nlet predicate2 = NSPredicate(format: \"'age' \u003c 20\")\n\nlet isTeenager = predicate1 \u0026\u0026 predicate2\nlet isNotTeenager = !isTeenager\nlet alsoIsNotTeenager = !predicate1 || !predicate2\nlet alsoIsTeenager = [predicate1, predicate2].joined(with: .and)\n```\n\nYou can also use `KeyPath` on `NSManagedObject` subclasses to make simple predicates:\n\n```swift\n// Simple Equatable or Comparable KeyPaths allow this kind of NSPredicate creation\nlet isOlderThanDirt = \\Person.age \u003e 1000\nlet notFred = \\Person.name != \"Fred\"\n\n// Or wrap the KeyPath in parentheses for further NSPredicate functions:\nlet isTeenager = (\\Person.age).between(13...19)\nlet isOddTeen = (\\Person.age).in([11, 13, 15, 17, 19])\n\n// String properties can have comparison options, too:\nlet definitelyNotFred = (\\Person.name).notEqual(\n            to: \"Fred\",\n            options: .caseAndDiacriticInsensitive)\n```\n\nFor a more robust, elegant, and type-safe `NSPredicate` system, check out [PredicateKit](\u003chttps://github.com/ftchirou/PredicateKit\u003e)\n\n## Further Plans\n\n**RCDataKit** is a work-in-progress… here are a few general improvements I currently have in mind:\n\n- 🚨 Better (or any) error handling.\n- 🤠 Improved versatility in `DataStack` protocol\n- 🚜 Combine publishers\n- 🛩️ Async/Await helpers\n- 💭 CloudKit integration\n- 🚧 More testing\n\n## Contribution/Feedback\n\nSince it’s a work-in-progress, I’m happy to receive suggestions, feedback, or contributions. Create an issue or pull request if you like.\n\nAnd please browse the code and use it however you please. I hope that it can help you learn a few things about Core Data that you can use in your own projects. Cheers!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frccoop%2Frcdatakit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frccoop%2Frcdatakit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frccoop%2Frcdatakit/lists"}