JohnEstropia
CoreStore
Swift

Unleashing the real power of Core Data with the elegance and safety of Swift

Last updated Jul 7, 2026
4.1k
Stars
259
Forks
95
Issues
0
Stars/day
Attention Score
96
Language breakdown
Swift 99.9%
Ruby 0.1%
Objective-C 0.1%
Files click to expand
README

CoreStore

Unleashing the real power of Core Data with the elegance and safety of Swift

Build Status Last Commit Platform License

Dependency managers
Cocoapods compatible Carthage compatible Swift Package Manager compatible

Contact
Join us on Slack! Reach me on Twitter! Sponsor

Upgrading from previous CoreStore versions? Check out the 🆕 features and make sure to read the Change logs.

CoreStore is part of the Swift Source Compatibility projects.

Contents

- Setting up - In-memory store - Local store - Migrations - Declaring model versions - Starting migrations - Progressive migrations - Forecasting migrations - Custom migrations - Saving and processing transactions - Transaction types - Asynchronous transactions - Synchronous transactions - Unsafe transactions - Creating objects - Updating objects - Deleting objects - Passing objects safely - Importing data - Fetching and querying - From clause - Fetching - Where clause - OrderBy clause - Tweak clause - Querying - Select<T> clause - GroupBy clause - Logging and error reporting - Observing changes and notifications - Observe a single property - Observe a single object's updates - Observe a single object's per-property updates - Observe a diffable list - Observe detailed list changes - Type-safe CoreStoreObjects - New @Field Property Wrapper syntax - @Field.Stored - @Field.Virtual - @Field.Coded - @Field.Relationship - @Field usage notes - VersionLocks - Reactive Programming - RxSwift - 🆕Combine - 🆕DataStack.reactive - 🆕ListPublisher.reactive - 🆕ObjectPublisher.reactive - 🆕SwiftUI Utilities - 🆕SwiftUI Views - 🆕ListReader - 🆕ObjectReader - 🆕SwiftUI Property Wrappers - 🆕ListState - 🆕ObjectState - 🆕SwiftUI Extensions - 🆕ForEach

TL;DR (a.k.a. sample codes)

Pure-Swift models: <pre><code class="lang-swift">class Person: CoreStoreObject { @Field.Stored(&quot;name&quot;) var name: String = &quot;&quot; @Field.Relationship(&quot;pets&quot;, inverse: \Dog.$master) var pets: Set&lt;Dog&gt; }</code></pre> (Classic NSManagedObjects also supported)

Setting-up with progressive migration support: <pre><code class="lang-swift">dataStack = DataStack( xcodeModelName: &quot;MyStore&quot;, migrationChain: [&quot;MyStore&quot;, &quot;MyStoreV2&quot;, &quot;MyStoreV3&quot;] )</code></pre>

Adding a store: <pre><code class="lang-swift">dataStack.addStorage( SQLiteStore(fileName: &quot;MyStore.sqlite&quot;), completion: { (result) -&gt; Void in // ... } )</code></pre>

Starting transactions: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let person = transaction.create(Into&lt;Person&gt;()) person.name = &quot;John Smith&quot; person.age = 42 }, completion: { (result) -&gt; Void in switch result { case .success: print(&quot;success!&quot;) case .failure(let error): print(error) } } )</code></pre>

Fetching objects (simple): <pre><code class="lang-swift">let people = try dataStack.fetchAll(From&lt;Person&gt;())</code></pre>

Fetching objects (complex): <pre><code class="lang-swift">let people = try dataStack.fetchAll( From&lt;Person&gt;() .where(\.age &gt; 30), .orderBy(.ascending(\.name), .descending(.\age)), .tweak({ $0.includesPendingChanges = false }) )</code></pre>

Querying values: <pre><code class="lang-swift">let maxAge = try dataStack.queryValue( From&lt;Person&gt;() .select(Int.self, .maximum(\.age)) )</code></pre>

But really, there's a reason I wrote this huge README. Read up on the details!

Check out the Demo app project for sample codes as well!

Why use CoreStore?

CoreStore was (and is) heavily shaped by real-world needs of developing data-dependent apps. It enforces safe and convenient Core Data usage while letting you take advantage of the industry's encouraged best practices.

Features

  • 🆕SwiftUI and Combine API utilities. ListPublishers and ObjectPublishers now have their @ListState and @ObjectState SwiftUI property wrappers. Combine Publisher s are also available through the ListPublisher.reactive, ObjectPublisher.reactive, and DataStack.reactive namespaces.
  • Backwards-portable DiffableDataSources implementation! UITableViews and UICollectionViews now have a new ally: ListPublishers provide diffable snapshots that make reloading animations very easy and very safe. Say goodbye to UITableViews and UICollectionViews reload errors!
  • 💎Tight design around Swift’s code elegance and type safety. CoreStore fully utilizes Swift's community-driven language features.
  • 🚦Safer concurrency architecture. CoreStore makes it hard to fall into common concurrency mistakes. The main NSManagedObjectContext is strictly read-only, while all updates are done through serial transactions. (See Saving and processing transactions)
  • 🔍Clean fetching and querying API. Fetching objects is easy, but querying for raw aggregates (min, max, etc.) and raw property values is now just as convenient. (See Fetching and querying)
  • 🔭Type-safe, easy to configure observers. You don't have to deal with the burden of setting up NSFetchedResultsControllers and KVO. As an added bonus, list and object observable types all support multiple observers. This means you can have multiple view controllers efficiently share a single resource! (See Observing changes and notifications)
  • 📥Efficient importing utilities. Map your entities once with their corresponding import source (JSON for example), and importing from transactions becomes elegant. Uniquing is also done with an efficient find-and-replace algorithm. (See Importing data)
  • 🗑Say goodbye to .xcdatamodeld files! While CoreStore supports NSManagedObjects, it offers CoreStoreObject whose subclasses can declare type-safe properties all in Swift code without the need to maintain separate resource files for the models. As bonus, these special properties support custom types, and can be used to create type-safe keypaths and queries. (See Type-safe CoreStoreObjects)
  • 🔗Progressive migrations. No need to think how to migrate from all previous model versions to your latest model. Just tell the DataStack the sequence of version strings (MigrationChains) and CoreStore will automatically use progressive migrations when needed. (See Migrations)
  • Easier custom migrations. Say goodbye to .xcmappingmodel files; CoreStore can now infer entity mappings when possible, while still allowing an easy way to write custom mappings. (See Migrations)
  • 📝Plug-in your own logging framework. Although a default logger is built-in, all logging, asserting, and error reporting can be funneled to CoreStoreLogger protocol implementations. (See Logging and error reporting)
  • ⛓Heavy support for multiple persistent stores per data stack. CoreStore lets you manage separate stores in a single DataStack, just the way .xcdatamodeld configurations are designed to. CoreStore will also manage one stack by default, but you can create and manage as many as you need. (See Setting up)
  • 🎯Free to name entities and their class names independently. CoreStore gets around a restriction with other Core Data wrappers where the entity name should be the same as the NSManagedObject subclass name. CoreStore loads entity-to-class mappings from the managed object model file, so you can assign independent names for the entities and their class names.
  • 📙Full Documentation. No magic here; all public classes, functions, properties, etc. have detailed Apple Docs. This README also introduces a lot of concepts and explains a lot of CoreStore's behavior.
  • ℹ️Informative (and pretty) logs. All CoreStore and Core Data-related types now have very informative and pretty print outputs! (See Logging and error reporting)
  • 🛡More extensive Unit Tests. Extending CoreStore is safe without having to worry about breaking old behavior.
Have ideas that may benefit other Core Data users? Feature Requests are welcome!

Architecture

For maximum safety and performance, CoreStore will enforce coding patterns and practices it was designed for. (Don't worry, it's not as scary as it sounds.) But it is advisable to understand the "magic" of CoreStore before you use it in your apps.

If you are already familiar with the inner workings of CoreData, here is a mapping of CoreStore abstractions:

| Core Data | CoreStore | | --- | --- | | NSPersistentContainer<br />(.xcdatamodeld file) | DataStack | | NSPersistentStoreDescription<br />("Configuration"s in the .xcdatamodeld file) | StorageInterface implementations<br />(InMemoryStore, SQLiteStore) | | NSManagedObjectContext | BaseDataTransaction subclasses<br />(SynchronousDataTransaction, AsynchronousDataTransaction, UnsafeDataTransaction) |

A lot of Core Data wrapper libraries set up their NSManagedObjectContexts this way:

<img src="https://cloud.githubusercontent.com/assets/3029684/16707160/984ef25c-4600-11e6-869f-8db7d2c63668.png" alt="nested contexts" height=380 />

Nesting saves from child context to the root context ensures maximum data integrity between contexts without blocking the main queue. But <a href="http://floriankugler.com/2013/04/29/concurrent-core-data-stack-performance-shootout/">in reality</a>, merging contexts is still by far faster than saving contexts. CoreStore's DataStack takes the best of both worlds by treating the main NSManagedObjectContext as a read-only context (or "viewContext"), and only allows changes to be made within transactions on the child context:

<img src="https://cloud.githubusercontent.com/assets/3029684/16707161/9adeb962-4600-11e6-8bc8-4ec85764dba4.png" alt="nested contexts and merge hybrid" height=292 />

This allows for a butter-smooth main thread, while still taking advantage of safe nested contexts.

Setting up

The simplest way to initialize CoreStore is to add a default store to the default stack: <pre><code class="lang-swift">try CoreStoreDefaults.dataStack.addStorageAndWait()</code></pre> This one-liner does the following:
  • Triggers the lazy-initialization of CoreStoreDefaults.dataStack with a default DataStack
  • Sets up the stack's NSPersistentStoreCoordinator, the root saving NSManagedObjectContext, and the read-only main NSManagedObjectContext
  • Adds an SQLiteStore in the "Application Support/<bundle id>" directory (or the "Caches/<bundle id>" directory on tvOS) with the file name "[App bundle name].sqlite"
  • Creates and returns the NSPersistentStore instance on success, or an NSError on failure
For most cases, this configuration is enough as it is. But for more hardcore settings, refer to this extensive example: <pre><code class="lang-swift">let dataStack = DataStack( xcodeModelName: &quot;MyModel&quot;, // loads from the &quot;MyModel.xcdatamodeld&quot; file migrationChain: [&quot;MyStore&quot;, &quot;MyStoreV2&quot;, &quot;MyStoreV3&quot;] // model versions for progressive migrations ) let migrationProgress = dataStack.addStorage( SQLiteStore( fileURL: sqliteFileURL, // set the target file URL for the sqlite file configuration: &quot;Config2&quot;, // use entities from the &quot;Config2&quot; configuration in the .xcdatamodeld file localStorageOptions: .recreateStoreOnModelMismatch // if migration paths cannot be resolved, recreate the sqlite file ), completion: { (result) -&gt; Void in switch result { case .success(let storage): print(&quot;Successfully added sqlite store: \(storage)&quot;) case .failure(let error): print(&quot;Failed adding sqlite store with error: \(error)&quot;) } } )

CoreStoreDefaults.dataStack = dataStack // pass the dataStack to CoreStore for easier access later on</code></pre>

> 💡If you have never heard of "Configurations", you'll find them in your .xcdatamodeld file > <img src="https://cloud.githubusercontent.com/assets/3029684/8333192/e52cfaac-1acc-11e5-9902-08724f9f1324.png" alt="xcode configurations screenshot" height=212 />

In our sample code above, note that you don't need to do the CoreStoreDefaults.dataStack = dataStack line. You can just as well hold a reference to the DataStack like below and call all its instance methods directly: <pre><code class="lang-swift">class MyViewController: UIViewController { let dataStack = DataStack(xcodeModelName: &quot;MyModel&quot;) // keep reference to the stack override func viewDidLoad() { super.viewDidLoad() do { try self.dataStack.addStorageAndWait(SQLiteStore.self) } catch { // ... } } func methodToBeCalledLaterOn() { let objects = self.dataStack.fetchAll(From&lt;MyEntity&gt;()) print(objects) } }</code></pre>

> 💡By default, CoreStore will initialize NSManagedObjects from .xcdatamodeld files, but you can create models completely from source code using CoreStoreObjects and CoreStoreSchema. To use this feature, refer to Type-safe CoreStoreObjects.

Notice that in our previous examples, addStorageAndWait(:) and addStorage(:completion:) both accept either InMemoryStore, or SQLiteStore. These implement the StorageInterface protocol.

In-memory store

The most basic
StorageInterface concrete type is the InMemoryStore, which just stores objects in memory. Since InMemoryStores always start with a fresh empty data, they do not need any migration information. <pre><code class="lang-swift">try dataStack.addStorageAndWait( InMemoryStore( configuration: &quot;Config2&quot; // optional. Use entities from the &quot;Config2&quot; configuration in the .xcdatamodeld file ) )</code></pre> Asynchronous variant: <pre><code class="lang-swift">try dataStack.addStorage( InMemoryStore( configuration: &quot;Config2 ), completion: { storage in // ... } )</code></pre>

(A reactive-programming variant of this method is explained in detail in the section on DataStack Combine publishers)

Local Store

The most common
StorageInterface you will probably use is the SQLiteStore, which saves data in a local SQLite file. <pre><code class="lang-swift">let migrationProgress = dataStack.addStorage( SQLiteStore( fileName: &quot;MyStore.sqlite&quot;, configuration: &quot;Config2&quot;, // optional. Use entities from the &quot;Config2&quot; configuration in the .xcdatamodeld file migrationMappingProviders: [Bundle.main], // optional. The bundles that contain required .xcmappingmodel files localStorageOptions: .recreateStoreOnModelMismatch // optional. Provides settings that tells the DataStack how to setup the persistent store ), completion: { / ... / } )</code></pre> Refer to the SQLiteStore.swift source documentation for detailed explanations for each of the default values.

CoreStore can decide the default values for these properties, so SQLiteStores can be initialized with no arguments: <pre><code class="lang-swift">try dataStack.addStorageAndWait(SQLiteStore())</code></pre>

(The asynchronous variant of this method is explained further in the next section on Migrations, and a reactive-programming variant in the section on DataStack Combine publishers)

The file-related properties of SQLiteStore are actually requirements of another protocol that it implements, the LocalStorage protocol: <pre><code class="lang-swift">public protocol LocalStorage: StorageInterface { var fileURL: NSURL { get } var migrationMappingProviders: [SchemaMappingProvider] { get } var localStorageOptions: LocalStorageOptions { get } func dictionary(forOptions: LocalStorageOptions) -&gt; [String: AnyObject]? func cs_eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws }</code></pre> If you have custom NSIncrementalStore or NSAtomicStore subclasses, you can implement this protocol and use it similarly to SQLiteStore.

Migrations

Declaring model versions

Model versions are now expressed as a first-class protocol,
DynamicSchema. CoreStore currently supports the following schema classes:
  • XcodeDataModelSchema: a model version with entities loaded from a .xcdatamodeld file.
  • CoreStoreSchema: a model version created with CoreStoreObject entities. (See Type-safe CoreStoreObjects)
  • UnsafeDataModelSchema: a model version created with an existing NSManagedObjectModel instance.
All the DynamicSchema for all model versions are then collected within a single SchemaHistory instance, which is then handed to the DataStack. Here are some common use cases:

Multiple model versions grouped in a .xcdatamodeld file (Core Data standard method) <pre><code class="lang-swift">CoreStoreDefaults.dataStack = DataStack( xcodeModelName: &quot;MyModel&quot;, bundle: Bundle.main, migrationChain: [&quot;MyAppModel&quot;, &quot;MyAppModelV2&quot;, &quot;MyAppModelV3&quot;, &quot;MyAppModelV4&quot;] )</code></pre>

CoreStoreSchema-based model version (No .xcdatamodeld file needed) (For more details, see also Type-safe CoreStoreObjects) <pre><code class="lang-swift">class Animal: CoreStoreObject { // ... } class Dog: Animal { // ... } class Person: CoreStoreObject { // ... }

CoreStoreDefaults.dataStack = DataStack( CoreStoreSchema( modelVersion: &quot;V1&quot;, entities: [ Entity&lt;Animal&gt;(&quot;Animal&quot;, isAbstract: true), Entity&lt;Dog&gt;(&quot;Dog&quot;), Entity&lt;Person&gt;(&quot;Person&quot;) ] ) )</code></pre>

Models in a .xcdatamodeld file during past app versions, but migrated to the new CoreStoreSchema method <pre><code class="lang-swift">class Animal: CoreStoreObject { // ... } class Dog: Animal { // ... } class Person: CoreStoreObject { // ... }

let legacySchema = XcodeDataModelSchema.from( modelName: &quot;MyModel&quot;, // .xcdatamodeld name bundle: bundle, migrationChain: [&quot;MyAppModel&quot;, &quot;MyAppModelV2&quot;, &quot;MyAppModelV3&quot;, &quot;MyAppModelV4&quot;] ) let newSchema = CoreStoreSchema( modelVersion: &quot;V1&quot;, entities: [ Entity&lt;Animal&gt;(&quot;Animal&quot;, isAbstract: true), Entity&lt;Dog&gt;(&quot;Dog&quot;), Entity&lt;Person&gt;(&quot;Person&quot;) ] ) CoreStoreDefaults.dataStack = DataStack( schemaHistory: SchemaHistory( legacySchema + [newSchema], migrationChain: [&quot;MyAppModel&quot;, &quot;MyAppModelV2&quot;, &quot;MyAppModelV3&quot;, &quot;MyAppModelV4&quot;, &quot;V1&quot;] ) )</code></pre>

CoreStoreSchema-based model versions with progressive migration <pre><code class="lang-swift">typealias Animal = V2.Animal typealias Dog = V2.Dog typealias Person = V2.Person enum V2 { class Animal: CoreStoreObject { // ... } class Dog: Animal { // ... } class Person: CoreStoreObject { // ... } } enum V1 { class Animal: CoreStoreObject { // ... } class Dog: Animal { // ... } class Person: CoreStoreObject { // ... } }

CoreStoreDefaults.dataStack = DataStack( CoreStoreSchema( modelVersion: &quot;V1&quot;, entities: [ Entity&lt;V1.Animal&gt;(&quot;Animal&quot;, isAbstract: true), Entity&lt;V1.Dog&gt;(&quot;Dog&quot;), Entity&lt;V1.Person&gt;(&quot;Person&quot;) ] ), CoreStoreSchema( modelVersion: &quot;V2&quot;, entities: [ Entity&lt;V2.Animal&gt;(&quot;Animal&quot;, isAbstract: true), Entity&lt;V2.Dog&gt;(&quot;Dog&quot;), Entity&lt;V2.Person&gt;(&quot;Person&quot;) ] ), migrationChain: [&quot;V1&quot;, &quot;V2&quot;] )</code></pre>

Starting migrations

We have seen
addStorageAndWait(...) used to initialize our persistent store. As the method name's ~AndWait suffix suggests though, this method blocks so it should not do long tasks such as data migrations. In fact CoreStore will only attempt a synchronous lightweight migration if you explicitly provide the .allowSynchronousLightweightMigration option: <pre><code class="lang-swift">try dataStack.addStorageAndWait( SQLiteStore( fileURL: sqliteFileURL, localStorageOptions: .allowSynchronousLightweightMigration ) }</code></pre> if you do so, any model mismatch will be thrown as an error.

In general though, if migrations are expected the asynchronous variant addStorage(_:completion:) method is recommended instead: <pre><code class="lang-swift">let migrationProgress: Progress? = try dataStack.addStorage( SQLiteStore( fileName: &quot;MyStore.sqlite&quot;, configuration: &quot;Config2&quot; ), completion: { (result) -&gt; Void in switch result { case .success(let storage): print(&quot;Successfully added sqlite store: \(storage)&quot;) case .failure(let error): print(&quot;Failed adding sqlite store with error: \(error)&quot;) } } )</code></pre> The completion block reports a SetupResult that indicates success or failure.

(A reactive-programming variant of this method is explained further in the section on DataStack Combine publishers)

Notice that this method also returns an optional Progress. If nil, no migrations are needed, thus progress reporting is unnecessary as well. If not nil, you can use this to track migration progress by using standard KVO on the "fractionCompleted" key, or by using a closure-based utility exposed in Progress+Convenience.swift: <pre><code class="lang-swift">migrationProgress?.setProgressHandler { [weak self] (progress) -&gt; Void in self?.progressView?.setProgress(Float(progress.fractionCompleted), animated: true) self?.percentLabel?.text = progress.localizedDescription // &quot;50% completed&quot; self?.stepLabel?.text = progress.localizedAdditionalDescription // &quot;0 of 2&quot; }</code></pre> This closure is executed on the main thread so UIKit and AppKit calls can be done safely.

Progressive migrations

By default, CoreStore uses Core Data's default automatic migration mechanism. In other words, CoreStore will try to migrate the existing persistent store until it matches the
SchemaHistory's currentModelVersion. If no mapping model path is found from the store's version to the data model's version, CoreStore gives up and reports an error.

The DataStack lets you specify hints on how to break a migration into several sub-migrations using a MigrationChain. This is typically passed to the DataStack initializer and will be applied to all stores added to the DataStack with addSQLiteStore(...) and its variants: <pre><code class="lang-swift">let dataStack = DataStack(migrationChain: [&quot;MyAppModel&quot;, &quot;MyAppModelV2&quot;, &quot;MyAppModelV3&quot;, &quot;MyAppModelV4&quot;])</code></pre> The most common usage is to pass in the model version (.xcdatamodeld version names for NSManagedObjects, or the modelName for CoreStoreSchemas) in increasing order as above.

For more complex, non-linear migration paths, you can also pass in a version tree that maps the key-values to the source-destination versions: <pre><code class="lang-swift">let dataStack = DataStack(migrationChain: [ &quot;MyAppModel&quot;: &quot;MyAppModelV3&quot;, &quot;MyAppModelV2&quot;: &quot;MyAppModelV4&quot;, &quot;MyAppModelV3&quot;: &quot;MyAppModelV4&quot; ])</code></pre> This allows for different migration paths depending on the starting version. The example above resolves to the following paths:

  • MyAppModel-MyAppModelV3-MyAppModelV4
  • MyAppModelV2-MyAppModelV4
  • MyAppModelV3-MyAppModelV4
Initializing with empty values (either
nil, [], or [:]) instructs the DataStack to disable progressive migrations and revert to the default migration behavior (i.e. use the .xcdatamodeld's current version as the final version): <pre><code class="lang-swift">let dataStack = DataStack(migrationChain: nil)</code></pre>

The MigrationChain is validated when passed to the DataStack and unless it is empty, will raise an assertion if any of the following conditions are met:

  • a version appears twice in an array
  • a version appears twice as a key in a dictionary literal
  • a loop is found in any of the paths
> ⚠️Important: If a MigrationChain is specified, the .xcdatamodeld's "Current Version" will be bypassed and the MigrationChain's leafmost version will be the DataStack's base model version.

Forecasting migrations

Sometimes migrations are huge and you may want prior information so your app could display a loading screen, or to display a confirmation dialog to the user. For this, CoreStore provides a requiredMigrationsForStorage(:) method you can use to inspect a persistent store before you actually call addStorageAndWait(:) or addStorage(_:completion:): <pre><code class="lang-swift">do { let storage = SQLiteStorage(fileName: &quot;MyStore.sqlite&quot;) let migrationTypes: [MigrationType] = try dataStack.requiredMigrationsForStorage(storage) if migrationTypes.count &gt; 1 || (migrationTypes.filter { $0.isHeavyweightMigration }.count) &gt; 0 { // ... will migrate more than once. Show special waiting screen } else if migrationTypes.count &gt; 0 { // ... will migrate just once. Show simple activity indicator } else { // ... Do nothing } dataStack.addStorage(storage, completion: { / ... / }) } catch { // ... either inspection of the store failed, or if no mapping model was found/inferred }</code></pre> requiredMigrationsForStorage(_:) returns an array of MigrationTypes, where each item in the array may be either of the following values: <pre><code class="lang-swift">case lightweight(sourceVersion: String, destinationVersion: String) case heavyweight(sourceVersion: String, destinationVersion: String)</code></pre> Each MigrationType indicates the migration type for each step in the MigrationChain. Use these information as fit for your app.

Custom migrations

CoreStore offers several ways to declare migration mappings:

  • CustomSchemaMappingProvider: A mapping provider that infers mapping initially, but also accepts custom mappings for specified entities. This was added to support custom migrations with CoreStoreObjects as well, but may also be used with NSManagedObjects.
  • XcodeSchemaMappingProvider: A mapping provider which loads entity mappings from .xcmappingmodel files in a specified Bundle.
  • InferredSchemaMappingProvider: The default mapping provider which tries to infer model migration between two DynamicSchema versions either by searching all .xcmappingmodel files from Bundle.allBundles, or by relying on lightweight migration if possible.
These mapping providers conform to SchemaMappingProvider and can be passed to SQLiteStore's initializer: <pre><code class="lang-swift">let dataStack = DataStack(migrationChain: [&quot;MyAppModel&quot;, &quot;MyAppModelV2&quot;, &quot;MyAppModelV3&quot;, &quot;MyAppModelV4&quot;]) _ = try dataStack.addStorage( SQLiteStore( fileName: &quot;MyStore.sqlite&quot;, migrationMappingProviders: [ XcodeSchemaMappingProvider(from: &quot;V1&quot;, to: &quot;V2&quot;, mappingModelBundle: Bundle.main), CustomSchemaMappingProvider(from: &quot;V2&quot;, to: &quot;V3&quot;, entityMappings: [.deleteEntity(&quot;Person&quot;) ]) ] ), completion: { (result) -&gt; Void in // ... } )</code></pre>

For version migrations present in the DataStack's MigrationChain but not handled by any of the SQLiteStore's migrationMappingProviders array, CoreStore will automatically try to use InferredSchemaMappingProvider as fallback. Finally if the InferredSchemaMappingProvider could not resolve any mapping, the migration will fail and the DataStack.addStorage(...) method will report the failure.

For CustomSchemaMappingProvider, more granular updates are supported through the dynamic objects UnsafeSourceObject and UnsafeDestinationObject. The example below allows the migration to conditionally ignore some objects: <pre><code class="lang-swift">let personv2tov3mapping = CustomSchemaMappingProvider( from: &quot;V2&quot;, to: &quot;V3&quot;, entityMappings: [ .transformEntity( sourceEntity: &quot;Person&quot;, destinationEntity: &quot;Person&quot;, transformer: { (sourceObject: UnsafeSourceObject, createDestinationObject: () -&gt; UnsafeDestinationObject) in if (sourceObject[&quot;isVeryOldAccount&quot;] as! Bool?) == true { return // this account is too old, don&#39;t migrate } // migrate the rest let destinationObject = createDestinationObject() destinationObject.enumerateAttributes { (attribute, sourceAttribute) in if let sourceAttribute = sourceAttribute { destinationObject[attribute] = sourceObject[sourceAttribute] } } ) ] ) SQLiteStore( fileName: &quot;MyStore.sqlite&quot;, migrationMappingProviders: [personv2tov3mapping] )</code></pre> The UnsafeSourceObject is a read-only proxy for an object existing in the source model version. The UnsafeDestinationObject is a read-write object that is inserted (optionally) to the destination model version. Both classes' properties are accessed through key-value-coding.

Saving and processing transactions

To ensure deterministic state for objects in the read-only
NSManagedObjectContext, CoreStore does not expose API's for updating and saving directly from the main context (or any other context for that matter.) Instead, you spawn transactions from DataStack instances: <pre><code class="lang-swift">let dataStack = self.dataStack dataStack.perform( asynchronous: { (transaction) -&gt; Void in // make changes }, completion: { (result) -&gt; Void in // ... } )</code></pre> Transaction closures automatically save changes once the closures completes. To cancel and rollback a transaction, throw a CoreStoreError.userCancelled from inside the closure by calling try transaction.cancel(): <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in // ... if shouldCancel { try transaction.cancel() } // ... }, completion: { (result) -&gt; Void in if case .failure(.userCancelled) = result { // ... cancelled } } )</code></pre> > ⚠️Important: Never use try? or try! on a transaction.cancel() call. Always use try. Using try? will swallow the cancellation and the transaction will proceed to save as normal. Using try! will crash the app as transaction.cancel() will always throw an error.

The examples above use perform(asynchronous:...), but there are actually 3 types of transactions at your disposal: asynchronous, synchronous, and unsafe.

Transaction types

Asynchronous transactions

are spawned from
perform(asynchronous:...). This method returns immediately and executes its closure from a background serial queue. The return value for the closure is declared as a generic type, so any value returned from the closure can be passed to the completion result: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Bool in // make changes return transaction.hasChanges }, completion: { (result) -&gt; Void in switch result { case .success(let hasChanges): print(&quot;success! Has changes? \(hasChanges)&quot;) case .failure(let error): print(error) } } )</code></pre> The success and failure can also be declared as separate handlers: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Int in // make changes return transaction.delete(objects) }, success: { (numberOfDeletedObjects: Int) -&gt; Void in print(&quot;success! Deleted \(numberOfDeletedObjects) objects&quot;) }, failure: { (error) -&gt; Void in print(error) } )</code></pre>

> ⚠️Be careful when returning NSManagedObjects or CoreStoreObjects from the transaction closure. Those instances are for the transaction's use only. See Passing objects safely.

Transactions created from perform(asynchronous:...) are instances of AsynchronousDataTransaction.

Synchronous transactions

are created from
perform(synchronous:...). While the syntax is similar to its asynchronous counterpart, perform(synchronous:...) waits for its transaction block to complete before returning: <pre><code class="lang-swift">let hasChanges = dataStack.perform( synchronous: { (transaction) -&gt; Bool in // make changes return transaction.hasChanges } )</code></pre> transaction above is a SynchronousDataTransaction instance.

Since perform(synchronous:...) technically blocks two queues (the caller's queue and the transaction's background queue), it is considered less safe as it's more prone to deadlock. Take special care that the closure does not block on any other external queues.

By default, perform(synchronous:...) will wait for observers such as ListMonitors to be notified before the method returns. This may cause deadlocks, especially if you are calling this from the main thread. To reduce this risk, you may try to set the waitForAllObservers: parameter to false. Doing so tells the SynchronousDataTransaction to block only until it completes saving. It will not wait for other context's to receive those changes. This reduces deadlock risk but may have surprising side-effects: <pre><code class="lang-swift">dataStack.perform( synchronous: { (transaction) in let person = transaction.create(Into&lt;Person&gt;()) person.name = &quot;John&quot; }, waitForAllObservers: false ) let newPerson = dataStack.fetchOne(From&lt;Person&gt;.where(\.name == &quot;John&quot;)) // newPerson may be nil! // The DataStack may have not yet received the update notification.</code></pre> Due to this complicated nature of synchronous transactions, if your app has very heavy transaction throughput it is highly recommended to use asynchronous transactions instead.

Unsafe transactions

are special in that they do not enclose updates within a closure: <pre><code class="lang-swift">let transaction = dataStack.beginUnsafe() // make changes downloadJSONWithCompletion({ (json) -&gt; Void in

// make other changes transaction.commit() }) downloadAnotherJSONWithCompletion({ (json) -&gt; Void in

// make some other changes transaction.commit() })</code></pre> This allows for non-contiguous updates. Do note that this flexibility comes with a price: you are now responsible for managing concurrency for the transaction. As uncle Ben said, "with great power comes great race conditions."

As the above example also shows, with unsafe transactions commit() can be called multiple times.

You've seen how to create transactions, but we have yet to see how to make creates, updates, and deletes. The 3 types of transactions above are all subclasses of BaseDataTransaction, which implements the methods shown below.

Creating objects

The create(...) method accepts an Into clause which specifies the entity for the object you want to create: <pre><code class="lang-swift">let person = transaction.create(Into&lt;MyPersonEntity&gt;())</code></pre> While the syntax is straightforward, CoreStore does not just naively insert a new object. This single line does the following:

  • Checks that the entity type exists in any of the transaction's parent persistent store
  • If the entity belongs to only one persistent store, a new object is inserted into that store and returned from create(...)
  • If the entity does not belong to any store, an assertion failure will be raised. This is a programmer error and should never occur in production code.
  • If the entity belongs to multiple stores, an assertion failure will be raised. This is also a programmer error and should never occur in production code. Normally, with Core Data you can insert an object in this state but saving the NSManagedObjectContext will always fail. CoreStore checks this for you at creation time when it makes sense (not during save).
If the entity exists in multiple configurations, you need to provide the configuration name for the destination persistent store: <pre><code class="lang-swift">let person = transaction.create(Into&lt;MyPersonEntity&gt;(&quot;Config1&quot;))</code></pre> or if the persistent store is the auto-generated "Default" configuration, specify nil: <pre><code class="lang-swift">let person = transaction.create(Into&lt;MyPersonEntity&gt;(nil))</code></pre> Note that if you do explicitly specify the configuration name, CoreStore will only try to insert the created object to that particular store and will fail if that store is not found; it will not fall back to any other configuration that the entity belongs to.

Updating objects

After creating an object from the transaction, you can simply update its properties as normal: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let person = transaction.create(Into&lt;MyPersonEntity&gt;()) person.name = &quot;John Smith&quot; person.age = 30 }, completion: { _ in } )</code></pre> To update an existing object, fetch the object's instance from the transaction: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let person = try transaction.fetchOne( From&lt;MyPersonEntity&gt;() .where(\.name == &quot;Jane Smith&quot;) ) person.age = person.age + 1 }, completion: { _ in } )</code></pre> (For more about fetching, see Fetching and querying)

Do not update an instance that was not created/fetched from the transaction. If you have a reference to the object already, use the transaction's edit(...) method to get an editable proxy instance for that object: <pre><code class="lang-swift">let jane: MyPersonEntity = // ...

dataStack.perform( asynchronous: { (transaction) -&gt; Void in // WRONG: jane.age = jane.age + 1 // RIGHT: let jane = transaction.edit(jane)! // using the same variable name protects us from misusing the non-transaction instance jane.age = jane.age + 1 }, completion: { _ in } )</code></pre> This is also true when updating an object's relationships. Make sure that the object assigned to the relationship is also created/fetched from the transaction: <pre><code class="lang-swift">let jane: MyPersonEntity = // ... let john: MyPersonEntity = // ...

dataStack.perform( asynchronous: { (transaction) -&gt; Void in // WRONG: jane.friends = [john] // RIGHT: let jane = transaction.edit(jane)! let john = transaction.edit(john)! jane.friends = NSSet(array: [john]) }, completion: { _ in } )</code></pre>

Deleting objects

Deleting an object is simpler because you can tell a transaction to delete an object directly without fetching an editable proxy (CoreStore does that for you): <pre><code class="lang-swift">let john: MyPersonEntity = // ...

dataStack.perform( asynchronous: { (transaction) -&gt; Void in transaction.delete(john) }, completion: { _ in } )</code></pre> or several objects at once: <pre><code class="lang-swift">let john: MyPersonEntity = // ... let jane: MyPersonEntity = // ...

dataStack.perform( asynchronous: { (transaction) -&gt; Void in try transaction.delete(john, jane) // try transaction.delete([john, jane]) is also allowed }, completion: { _ in } )</code></pre> If you do not have references yet to the objects to be deleted, transactions have a deleteAll(...) method you can pass a query to: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in try transaction.deleteAll( From&lt;MyPersonEntity&gt;() .where(\.age &gt; 30) ) }, completion: { _ in } )</code></pre>

Passing objects safely

Always remember that the DataStack and individual transactions manage different NSManagedObjectContexts so you cannot just use objects between them. That's why transactions have an edit(...) method: <pre><code class="lang-swift">let jane: MyPersonEntity = // ...

dataStack.perform( asynchronous: { (transaction) -&gt; Void in let jane = transaction.edit(jane)! jane.age = jane.age + 1 }, completion: { _ in } )</code></pre> But CoreStore, DataStack and BaseDataTransaction have a very flexible fetchExisting(...) method that you can pass instances back and forth with: <pre><code class="lang-swift">let jane: MyPersonEntity = // ...

dataStack.perform( asynchronous: { (transaction) -&gt; MyPersonEntity in let jane = transaction.fetchExisting(jane)! // instance for transaction jane.age = jane.age + 1 return jane }, success: { (transactionJane) in let jane = dataStack.fetchExisting(transactionJane)! // instance for DataStack print(jane.age) }, failure: { (error) in // ... } )</code></pre> fetchExisting(...) also works with multiple NSManagedObjects, CoreStoreObjects, or with NSManagedObjectIDs: <pre><code class="lang-swift">var peopleIDs: [NSManagedObjectID] = // ...

dataStack.perform( asynchronous: { (transaction) -&gt; Void in let jane = try transaction.fetchOne( From&lt;MyPersonEntity&gt;() .where(\.name == &quot;Jane Smith&quot;) ) jane.friends = NSSet(array: transaction.fetchExisting(peopleIDs)!) // ... }, completion: { _ in } )</code></pre>

Importing data

Some times, if not most of the time, the data that we save to Core Data comes from external sources such as web servers or external files. If you have a JSON dictionary for example, you may be extracting values as such: <pre><code class="lang-swift">let json: [String: Any] = // ... person.name = json[&quot;name&quot;] as? NSString person.age = json[&quot;age&quot;] as? NSNumber // ...</code></pre> If you have many attributes, you don't want to keep repeating this mapping everytime you want to import data. CoreStore lets you write the data mapping code just once, and all you have to do is call
importObject(...) or importUniqueObject(...) through BaseDataTransaction subclasses: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let json: [String: Any] = // ... try! transaction.importObject( Into&lt;MyPersonEntity&gt;(), source: json ) }, completion: { _ in } )</code></pre> To support data import for an entity, implement either ImportableObject or ImportableUniqueObject on the NSManagedObject or CoreStoreObject subclass:
  • ImportableObject: Use this protocol if the object have no inherent uniqueness and new objects should always be added when calling importObject(...).
  • ImportableUniqueObject: Use this protocol to specify a unique ID for an object that will be used to distinguish whether a new object should be created or if an existing object should be updated when calling importUniqueObject(...).
Both protocols require implementers to specify an ImportSource which can be set to any type that the object can extract data from: <pre><code class="lang-swift">typealias ImportSource = NSDictionary</code></pre> <pre><code class="lang-swift">typealias ImportSource = [String: Any]</code></pre> <pre><code class="lang-swift">typealias ImportSource = NSData</code></pre> You can even use external types from popular 3rd-party JSON libraries, or just simple tuples or primitives.

ImportableObject

ImportableObject is a very simple protocol: <pre><code class="lang-swift">public protocol ImportableObject: AnyObject { typealias ImportSource static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -&gt; Bool func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws }</code></pre> First, set ImportSource to the expected type of the data source: <pre><code class="lang-swift">typealias ImportSource = [String: Any]</code></pre> This lets us call importObject(_:source:) with any [String: Any] type as the argument to source: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let json: [String: Any] = // ... try! transaction.importObject( Into&lt;MyPersonEntity&gt;(), source: json ) // ... }, completion: { _ in } )</code></pre> The actual extraction and assignment of values should be implemented in the didInsert(from:in:) method of the ImportableObject protocol: <pre><code class="lang-swift">func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws { self.name = source[&quot;name&quot;] as? NSString self.age = source[&quot;age&quot;] as? NSNumber // ... }</code></pre> Transactions also let you import multiple objects at once using the importObjects(_:sourceArray:) method: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let jsonArray: [[String: Any]] = // ... try! transaction.importObjects( Into&lt;MyPersonEntity&gt;(), sourceArray: jsonArray // make sure this is of type Array&lt;MyPersonEntity.ImportSource&gt; ) // ... }, completion: { _ in } )</code></pre> Doing so tells the transaction to iterate through the array of import sources and calls shouldInsert(from:in:) on the ImportableObject to determine which instances should be created. You can do validations and return false from shouldInsert(from:in:) if you want to skip importing from a source and continue on with the other sources in the array.

If on the other hand, your validation in one of the sources failed in such a manner that all other sources should also be rolled back and cancelled, you can throw from within didInsert(from:in:): <pre><code class="lang-swift">func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws { self.name = source[&quot;name&quot;] as? NSString self.age = source[&quot;age&quot;] as? NSNumber // ... if self.name == nil { throw Errors.InvalidNameError } }</code></pre> Doing so can let you abandon an invalid transaction immediately: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let jsonArray: [[String: Any]] = // ...

try transaction.importObjects( Into&lt;MyPersonEntity&gt;(), sourceArray: jsonArray ) }, success: { // ... }, failure: { (error) in switch error { case Errors.InvalidNameError: print(&quot;Invalid name&quot;) // ... } } )</code></pre>

ImportableUniqueObject

Typically, we don't just keep creating objects every time we import data. Usually we also need to update already existing objects. Implementing the ImportableUniqueObject protocol lets you specify a "unique ID" that transactions can use to search existing objects before creating new ones: <pre><code class="lang-swift">public protocol ImportableUniqueObject: ImportableObject { typealias ImportSource typealias UniqueIDType: ImportableAttributeType

static var uniqueIDKeyPath: String { get } var uniqueIDValue: UniqueIDType { get set }

static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -&gt; Bool static func shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -&gt; Bool static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -&gt; UniqueIDType? func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws func update(from source: ImportSource, in transaction: BaseDataTransaction) throws }</code></pre> Notice that it has the same insert methods as ImportableObject, with additional methods for updates and for specifying the unique ID: <pre><code class="lang-swift">class var uniqueIDKeyPath: String { return #keyPath(MyPersonEntity.personID) } var uniqueIDValue: Int { get { return self.personID } set { self.personID = newValue } } class func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -&gt; Int? { return source[&quot;id&quot;] as? Int }</code></pre> For ImportableUniqueObject, the extraction and assignment of values should be implemented from the update(from:in:) method. The didInsert(from:in:) by default calls update(from:in:), but you can separate the implementation for inserts and updates if needed.

You can then create/update an object by calling a transaction's importUniqueObject(...) method: <pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let json: [String: Any] = // ... try! transaction.importUniqueObject( Into&lt;MyPersonEntity&gt;(), source: json ) // ... }, completion: { _ in } )</code></pre> or multiple objects at once with the importUniqueObjects(...) method:

<pre><code class="lang-swift">dataStack.perform( asynchronous: { (transaction) -&gt; Void in let jsonArray: [[String: AnyObject]] = // ... try! transaction.importUniqueObjects( Into&lt;MyPersonEntity&gt;(), sourceArray: jsonArray ) // ... }, completion: { _ in } )</code></pre> As with ImportableObject, you can control whether to skip importing an object by implementing shouldInsert(from:in:) and shouldUpdate(from:in:), or to cancel all objects by throwing an error from the uniqueID(from:in:), didInsert(from:in:) or update(from:in:) methods.

Fetching and Querying

Before we dive in, be aware that CoreStore distinguishes between fetching and querying:
  • A fetch executes searches from a specific transaction or data stack. This means fetches can include pending objects (i.e. before a transaction calls on commit().) Use fetches when:
- results need to be NSManagedObject or CoreStoreObject instances - unsaved objects should be included in the search (though fetches can be configured to exclude unsaved ones)
  • A query pulls data straight from the persistent store. This means faster searches when computing aggregates such as count, min, max, etc. Use queries when:
- you need to compute aggregate functions (see below for a list of supported functions) - results can be raw values like
NSStrings, NSNumbers, Ints, NSDates, an NSDictionary of key-values, or any type that conform to QueryableAttributeType. (See QueryableAttributeType.swift for a list of built-in types) - only values for specified attribute keys need to be included in the results - unsaved objects should be ignored

From clause

The search conditions for fetches and queries are specified using clauses. All fetches and queries require a From clause that indicates the target entity type: <pre><code class="lang-swift">let people = try dataStack.fetchAll(From&lt;MyPersonEntity&gt;())</code></pre> people in the example above will be of type [MyPersonEntity]. The From() clause indicates a fetch to all persistent stores that MyPersonEntity belong to.

If the entity exists in multiple configurations and you need to only search from a particular configuration, indicate in the From clause the configuration name for the destination persistent store: <pre><code class="lang-swift">let people = try dataStack.fetchAll(From&lt;MyPersonEntity&gt;(&quot;Config1&quot;)) // ignore objects in persistent stores other than the &quot;Config1&quot; configuration</code></pre> or if the persistent store is the auto-generated "Default" configuration, specify nil: <pre><code class="lang-swift">let person = try dataStack.fetchAll(From&lt;MyPersonEntity&gt;(nil))</code></pre> Now we know how to use a From clause, let's move on to fetching and querying.

Fetching

There are currently 5 fetch methods you can call from CoreStore, from a DataStack instance, or from a BaseDataTransaction instance. All of the methods below accept the same parameters: a required From clause, and an optional series of Where, OrderBy, and/or Tweak clauses.

  • fetchAll(...) - returns an array of all objects that match the criteria.
  • fetchOne(...) - returns the first object that match the criteria.
  • fetchCount(...) - returns the number of objects that match the criteria.
  • fetchObjectIDs(...) - returns an array of NSManagedObjectIDs for all objects that match the criteria.
  • fetchObjectID(...) - returns the NSManagedObjectIDs for the first objects that match the criteria.
Each method's purpose is straightforward, but we need to understand how to set the clauses for the fetch.

Where clause

The Where clause is CoreStore's NSPredicate wrapper. It specifies the search filter to use when fetching (or querying). It implements all initializers that NSPredicate does (except for -predicateWithBlock:, which Core Data does not support): <pre><code class="lang-swift">var people = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;(), Where&lt;MyPersonEntity&gt;(&quot;%K &gt; %d&quot;, &quot;age&quot;, 30) // string format initializer ) people = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;(), Where&lt;MyPersonEntity&gt;(true) // boolean initializer )</code></pre> If you do have an existing NSPredicate instance already, you can pass that to Where as well: <pre><code class="lang-swift">let predicate = NSPredicate(...) var people = dataStack.fetchAll( From&lt;MyPersonEntity&gt;(), Where&lt;MyPersonEntity&gt;(predicate) // predicate initializer )</code></pre> Where clauses are generic types. To avoid verbose repetition of the generic object type, fetch methods support Fetch Chain builders. We can also use Swift's Smart KeyPaths as the Where clause expression: <pre><code class="lang-swift">var people = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;() .where(\.age &gt; 30) // Type-safe! )</code></pre> Where clauses also implement the &&, ||, and ! logic operators, so you can provide logical conditions without writing too much AND, OR, and NOT strings: <pre><code class="lang-swift">var people = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;() .where(\.age &gt; 30 &amp;&amp; \.gender == &quot;M&quot;) )</code></pre> If you do not provide a Where clause, all objects that belong to the specified From will be returned.

OrderBy clause

The OrderBy clause is CoreStore's NSSortDescriptor wrapper. Use it to specify attribute keys in which to sort the fetch (or query) results with. <pre><code class="lang-swift">var mostValuablePeople = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;(), OrderBy&lt;MyPersonEntity&gt;(.descending(&quot;rating&quot;), .ascending(&quot;surname&quot;)) )</code></pre> As seen above, OrderBy accepts a list of SortKey enumeration values, which can be either .ascending or .descending. As with Where clauses, OrderBy clauses are also generic types. To avoid verbose repetition of the generic object type, fetch methods support Fetch Chain builders. We can also use Swift's Smart KeyPaths as the OrderBy clause expression: <pre><code class="lang-swift">var people = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;() .orderBy(.descending(\.rating), .ascending(\.surname)) // Type-safe! )</code></pre>

You can use the + and += operator to append OrderBys together. This is useful when sorting conditionally: <pre><code class="lang-swift">var orderBy = OrderBy&lt;MyPersonEntity&gt;(.descending(\.rating)) if sortFromYoungest { orderBy += OrderBy(.ascending(\.age)) } var mostValuablePeople = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;(), orderBy )</code></pre>

Tweak clause

The Tweak clause lets you, uh, tweak the fetch (or query). Tweak exposes the NSFetchRequest in a closure where you can make changes to its properties: <pre><code class="lang-swift">var people = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;(), Where&lt;MyPersonEntity&gt;(&quot;age &gt; %d&quot;, 30), OrderBy&lt;MyPersonEntity&gt;(.ascending(&quot;surname&quot;)), Tweak { (fetchRequest) -&gt; Void in fetchRequest.includesPendingChanges = false fetchRequest.returnsObjectsAsFaults = false fetchRequest.includesSubentities = false } )</code></pre> Tweak also supports Fetch Chain builders: <pre><code class="lang-swift">var people = try dataStack.fetchAll( From&lt;MyPersonEntity&gt;(), .where(\.age &gt; 30) .orderBy(.ascending(\.surname)) .tweak { $0.includesPendingChanges = false $0.returnsObjectsAsFaults = false $0.includesSubentities = false } )</code></pre> The clauses are evaluated the order they appear in the fetch/query, so you typically need to set Tweak as the last clause. Tweak's closure is executed only just before the fetch occurs, so make sure that any values captured by the closure is not prone to race conditions.

While Tweak lets you micro-configure the NSFetchRequest, note that CoreStore already preconfigured that NSFetchRequest to suitable defaults. Only use Tweak when you know what you are doing!

Querying

One of the functionalities overlooked by other Core Data wrapper libraries is raw properties fetching. If you are familiar with NSDictionaryResultType and -[NSFetchedRequest propertiesToFetch]`, you pro


README truncated. View on GitHub

© 2026 GitRepoTrend · JohnEstropia/CoreStore · Updated daily from GitHub