dataStack.perform(
asynchronous: { (transaction) -> Void in
let person = transaction.create(Into<Person>())
person.name = "John Smith"
person.age = 42
},
completion: { (result) -> Void in
switch result {
case .success: print("success!")
case .failure(let error): print(error)
}
}
)
Fetching objects (simple):
let people = try dataStack.fetchAll(From<Person>())
Fetching objects (complex):
let people = try dataStack.fetchAll(
From<Person>()
.where(\.age > 30),
.orderBy(.ascending(\.name), .descending(.\age)),
.tweak({ $0.includesPendingChanges = false })
)
Querying values:
let maxAge = try dataStack.queryValue(
From<Person>()
.select(Int.self, .maximum(\.age))
)
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 (.xcdatamodeld file)
DataStack
NSPersistentStoreDescription (“Configuration”s in the .xcdatamodeld file)
A lot of Core Data wrapper libraries set up their NSManagedObjectContexts this way:
Nesting saves from child context to the root context ensures maximum data integrity between contexts without blocking the main queue. But in reality, 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:
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:
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/“ directory (or the “Caches/“ 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:
let dataStack = DataStack(
xcodeModelName: "MyModel", // loads from the "MyModel.xcdatamodeld" file
migrationChain: ["MyStore", "MyStoreV2", "MyStoreV3"] // model versions for progressive migrations
)
let migrationProgress = dataStack.addStorage(
SQLiteStore(
fileURL: sqliteFileURL, // set the target file URL for the sqlite file
configuration: "Config2", // use entities from the "Config2" configuration in the .xcdatamodeld file
localStorageOptions: .recreateStoreOnModelMismatch // if migration paths cannot be resolved, recreate the sqlite file
),
completion: { (result) -> Void in
switch result {
case .success(let storage):
print("Successfully added sqlite store: \(storage)")
case .failure(let error):
print("Failed adding sqlite store with error: \(error)")
}
}
)
CoreStoreDefaults.dataStack = dataStack // pass the dataStack to CoreStore for easier access later on
💡If you have never heard of “Configurations”, you’ll find them in your .xcdatamodeld file
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:
class MyViewController: UIViewController {
let dataStack = DataStack(xcodeModelName: "MyModel") // 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<MyEntity>())
print(objects)
}
}
💡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.
try dataStack.addStorageAndWait(
InMemoryStore(
configuration: "Config2" // optional. Use entities from the "Config2" configuration in the .xcdatamodeld file
)
)
(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.
let migrationProgress = dataStack.addStorage(
SQLiteStore(
fileName: "MyStore.sqlite",
configuration: "Config2", // optional. Use entities from the "Config2" 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: { /* ... */ }
)
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:
try dataStack.addStorageAndWait(SQLiteStore())
(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:
public protocol LocalStorage: StorageInterface {
var fileURL: NSURL { get }
var migrationMappingProviders: [SchemaMappingProvider] { get }
var localStorageOptions: LocalStorageOptions { get }
func dictionary(forOptions: LocalStorageOptions) -> [String: AnyObject]?
func cs_eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws
}
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)
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:
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:
let migrationProgress: Progress? = try dataStack.addStorage(
SQLiteStore(
fileName: "MyStore.sqlite",
configuration: "Config2"
),
completion: { (result) -> Void in
switch result {
case .success(let storage):
print("Successfully added sqlite store: \(storage)")
case .failure(let error):
print("Failed adding sqlite store with error: \(error)")
}
}
)
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:
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:
let dataStack = DataStack(migrationChain:
["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"])
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:
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):
let dataStack = DataStack(migrationChain: nil)
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:):
do {
let storage = SQLiteStorage(fileName: "MyStore.sqlite")
let migrationTypes: [MigrationType] = try dataStack.requiredMigrationsForStorage(storage)
if migrationTypes.count > 1
|| (migrationTypes.filter { $0.isHeavyweightMigration }.count) > 0 {
// ... will migrate more than once. Show special waiting screen
}
else if migrationTypes.count > 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
}
requiredMigrationsForStorage(_:) returns an array of MigrationTypes, where each item in the array may be either of the following values:
case lightweight(sourceVersion: String, destinationVersion: String)
case heavyweight(sourceVersion: String, destinationVersion: String)
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:
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:
let person_v2_to_v3_mapping = CustomSchemaMappingProvider(
from: "V2",
to: "V3",
entityMappings: [
.transformEntity(
sourceEntity: "Person",
destinationEntity: "Person",
transformer: { (sourceObject: UnsafeSourceObject, createDestinationObject: () -> UnsafeDestinationObject) in
if (sourceObject["isVeryOldAccount"] as! Bool?) == true {
return // this account is too old, don't migrate
}
// migrate the rest
let destinationObject = createDestinationObject()
destinationObject.enumerateAttributes { (attribute, sourceAttribute) in
if let sourceAttribute = sourceAttribute {
destinationObject[attribute] = sourceObject[sourceAttribute]
}
}
)
]
)
SQLiteStore(
fileName: "MyStore.sqlite",
migrationMappingProviders: [person_v2_to_v3_mapping]
)
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:
let dataStack = self.dataStack
dataStack.perform(
asynchronous: { (transaction) -> Void in
// make changes
},
completion: { (result) -> Void in
// ...
}
)
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():
dataStack.perform(
asynchronous: { (transaction) -> Void in
// ...
if shouldCancel {
try transaction.cancel()
}
// ...
},
completion: { (result) -> Void in
if case .failure(.userCancelled) = result {
// ... cancelled
}
}
)
⚠️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:
dataStack.perform(
asynchronous: { (transaction) -> Bool in
// make changes
return transaction.hasChanges
},
completion: { (result) -> Void in
switch result {
case .success(let hasChanges): print("success! Has changes? \(hasChanges)")
case .failure(let error): print(error)
}
}
)
The success and failure can also be declared as separate handlers:
dataStack.perform(
asynchronous: { (transaction) -> Int in
// make changes
return transaction.delete(objects)
},
success: { (numberOfDeletedObjects: Int) -> Void in
print("success! Deleted \(numberOfDeletedObjects) objects")
},
failure: { (error) -> Void in
print(error)
}
)
⚠️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:
let hasChanges = dataStack.perform(
synchronous: { (transaction) -> Bool in
// make changes
return transaction.hasChanges
}
)
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:
dataStack.perform(
synchronous: { (transaction) in
let person = transaction.create(Into<Person>())
person.name = "John"
},
waitForAllObservers: false
)
let newPerson = dataStack.fetchOne(From<Person>.where(\.name == "John"))
// newPerson may be nil!
// The DataStack may have not yet received the update notification.
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:
let transaction = dataStack.beginUnsafe()
// make changes
downloadJSONWithCompletion({ (json) -> Void in
// make other changes
transaction.commit()
})
downloadAnotherJSONWithCompletion({ (json) -> Void in
// make some other changes
transaction.commit()
})
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:
let person = transaction.create(Into<MyPersonEntity>())
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:
let person = transaction.create(Into<MyPersonEntity>("Config1"))
or if the persistent store is the auto-generated “Default” configuration, specify nil:
let person = transaction.create(Into<MyPersonEntity>(nil))
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:
dataStack.perform(
asynchronous: { (transaction) -> Void in
let person = transaction.create(Into<MyPersonEntity>())
person.name = "John Smith"
person.age = 30
},
completion: { _ in }
)
To update an existing object, fetch the object’s instance from the transaction:
dataStack.perform(
asynchronous: { (transaction) -> Void in
let person = try transaction.fetchOne(
From<MyPersonEntity>()
.where(\.name == "Jane Smith")
)
person.age = person.age + 1
},
completion: { _ in }
)
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:
let jane: MyPersonEntity = // ...
dataStack.perform(
asynchronous: { (transaction) -> 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 }
)
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:
let jane: MyPersonEntity = // ...
let john: MyPersonEntity = // ...
dataStack.perform(
asynchronous: { (transaction) -> Void in
// WRONG: jane.friends = [john]
// RIGHT:
let jane = transaction.edit(jane)!
let john = transaction.edit(john)!
jane.friends = NSSet(array: [john])
},
completion: { _ in }
)
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):
let john: MyPersonEntity = // ...
dataStack.perform(
asynchronous: { (transaction) -> Void in
transaction.delete(john)
},
completion: { _ in }
)
or several objects at once:
let john: MyPersonEntity = // ...
let jane: MyPersonEntity = // ...
dataStack.perform(
asynchronous: { (transaction) -> Void in
try transaction.delete(john, jane)
// try transaction.delete([john, jane]) is also allowed
},
completion: { _ in }
)
If you do not have references yet to the objects to be deleted, transactions have a deleteAll(...) method you can pass a query to:
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:
let jane: MyPersonEntity = // ...
dataStack.perform(
asynchronous: { (transaction) -> Void in
let jane = transaction.edit(jane)!
jane.age = jane.age + 1
},
completion: { _ in }
)
But CoreStore, DataStack and BaseDataTransaction have a very flexible fetchExisting(...) method that you can pass instances back and forth with:
let jane: MyPersonEntity = // ...
dataStack.perform(
asynchronous: { (transaction) -> 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
// ...
}
)
fetchExisting(...) also works with multiple NSManagedObjects, CoreStoreObjects, or with NSManagedObjectIDs:
var peopleIDs: [NSManagedObjectID] = // ...
dataStack.perform(
asynchronous: { (transaction) -> Void in
let jane = try transaction.fetchOne(
From<MyPersonEntity>()
.where(\.name == "Jane Smith")
)
jane.friends = NSSet(array: transaction.fetchExisting(peopleIDs)!)
// ...
},
completion: { _ in }
)
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:
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:
dataStack.perform(
asynchronous: { (transaction) -> Void in
let json: [String: Any] = // ...
try! transaction.importObject(
Into<MyPersonEntity>(),
source: json
)
},
completion: { _ in }
)
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:
typealias ImportSource = NSDictionary
typealias ImportSource = [String: Any]
typealias ImportSource = NSData
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:
public protocol ImportableObject: AnyObject {
typealias ImportSource
static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws
}
First, set ImportSource to the expected type of the data source:
typealias ImportSource = [String: Any]
This lets us call importObject(_:source:) with any [String: Any] type as the argument to source:
dataStack.perform(
asynchronous: { (transaction) -> Void in
let json: [String: Any] = // ...
try! transaction.importObject(
Into<MyPersonEntity>(),
source: json
)
// ...
},
completion: { _ in }
)
The actual extraction and assignment of values should be implemented in the didInsert(from:in:) method of the ImportableObject protocol:
Transactions also let you import multiple objects at once using the importObjects(_:sourceArray:) method:
dataStack.perform(
asynchronous: { (transaction) -> Void in
let jsonArray: [[String: Any]] = // ...
try! transaction.importObjects(
Into<MyPersonEntity>(),
sourceArray: jsonArray // make sure this is of type Array<MyPersonEntity.ImportSource>
)
// ...
},
completion: { _ in }
)
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:):
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:
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) -> Bool
static func shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType?
func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws
func update(from source: ImportSource, in transaction: BaseDataTransaction) throws
}
Notice that it has the same insert methods as ImportableObject, with additional methods for updates and for specifying the unique ID:
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 -> Int? {
return source["id"] as? Int
}
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:
dataStack.perform(
asynchronous: { (transaction) -> Void in
let json: [String: Any] = // ...
try! transaction.importUniqueObject(
Into<MyPersonEntity>(),
source: json
)
// ...
},
completion: { _ in }
)
or multiple objects at once with the importUniqueObjects(...) method:
dataStack.perform(
asynchronous: { (transaction) -> Void in
let jsonArray: [[String: AnyObject]] = // ...
try! transaction.importUniqueObjects(
Into<MyPersonEntity>(),
sourceArray: jsonArray
)
// ...
},
completion: { _ in }
)
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:
let people = try dataStack.fetchAll(From<MyPersonEntity>())
people in the example above will be of type [MyPersonEntity]. The From<MyPersonEntity>() 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:
let people = try dataStack.fetchAll(From<MyPersonEntity>("Config1")) // ignore objects in persistent stores other than the "Config1" configuration
or if the persistent store is the auto-generated “Default” configuration, specify nil:
let person = try dataStack.fetchAll(From<MyPersonEntity>(nil))
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):
var people = try dataStack.fetchAll(
From<MyPersonEntity>(),
Where<MyPersonEntity>("%K > %d", "age", 30) // string format initializer
)
people = try dataStack.fetchAll(
From<MyPersonEntity>(),
Where<MyPersonEntity>(true) // boolean initializer
)
If you do have an existing NSPredicate instance already, you can pass that to Where as well:
let predicate = NSPredicate(...)
var people = dataStack.fetchAll(
From<MyPersonEntity>(),
Where<MyPersonEntity>(predicate) // predicate initializer
)
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:
var people = try dataStack.fetchAll(
From<MyPersonEntity>()
.where(\.age > 30) // Type-safe!
)
Where clauses also implement the &&, ||, and ! logic operators, so you can provide logical conditions without writing too much AND, OR, and NOT strings:
var people = try dataStack.fetchAll(
From<MyPersonEntity>()
.where(\.age > 30 && \.gender == "M")
)
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.
var mostValuablePeople = try dataStack.fetchAll(
From<MyPersonEntity>(),
OrderBy<MyPersonEntity>(.descending("rating"), .ascending("surname"))
)
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:
var people = try dataStack.fetchAll(
From<MyPersonEntity>()
.orderBy(.descending(\.rating), .ascending(\.surname)) // Type-safe!
)
You can use the + and += operator to append OrderBys together. This is useful when sorting conditionally:
var orderBy = OrderBy<MyPersonEntity>(.descending(\.rating))
if sortFromYoungest {
orderBy += OrderBy(.ascending(\.age))
}
var mostValuablePeople = try dataStack.fetchAll(
From<MyPersonEntity>(),
orderBy
)
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:
var people = try dataStack.fetchAll(
From<MyPersonEntity>(),
Where<MyPersonEntity>("age > %d", 30),
OrderBy<MyPersonEntity>(.ascending("surname")),
Tweak { (fetchRequest) -> Void in
fetchRequest.includesPendingChanges = false
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.includesSubentities = false
}
)
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 probably know how painful it is to setup a query for raw values and aggregate values. CoreStore makes this easy by exposing the 2 methods below:
queryValue(...) - returns a single raw value for an attribute or for an aggregate value. If there are multiple results, queryValue(...) only returns the first item.
queryAttributes(...) - returns an array of dictionaries containing attribute keys with their corresponding values.
Both methods above accept the same parameters: a required From clause, a required Select<T> clause, and an optional series of Where, OrderBy, GroupBy, and/or Tweak clauses.
Setting up the From, Where, OrderBy, and Tweak clauses is similar to how you would when fetching. For querying, you also need to know how to use the Select<T> and GroupBy clauses.
Select<T> clause
The Select<T> clause specifies the target attribute/aggregate key, as well as the expected return type:
The example above queries the “age” property for the first object that matches the Where condition. johnsAge will be bound to type Int?, as indicated by the Select<Int> generic type. For queryValue(...), types that conform to QueryableAttributeType are allowed as the return type (and therefore as the generic type for Select<T>).
For queryAttributes(...), only NSDictionary is valid for Select, thus you are allowed to omit the generic type:
let allAges = try dataStack.queryAttributes(
From<MyPersonEntity>(),
Select("age")
)
query methods also support Query Chain builders. We can also use Swift’s Smart KeyPaths to use in the expressions:
let johnsAge = try dataStack.queryValue(
From<MyPersonEntity>()
.select(\.age) // binds the result to Int
.where(\.name == "John Smith")
)
If you only need a value for a particular attribute, you can just specify the key name (like we did with Select<Int>("age")), but several aggregate functions can also be used as parameter to Select:
.average(...)
.count(...)
.maximum(...)
.minimum(...)
.sum(...)
let oldestAge = try dataStack.queryValue(
From<MyPersonEntity>(),
Select<Int>(.maximum("age"))
)
For queryAttributes(...) which returns an array of dictionaries, you can specify multiple attributes/aggregates to Select:
let personJSON = try dataStack.queryAttributes(
From<MyPersonEntity>(),
Select("name", "age")
)
The GroupBy clause lets you group results by a specified attribute/aggregate. This is useful only for queryAttributes(...) since queryValue(...) just returns the first value.
One unfortunate thing when using some third-party libraries is that they usually pollute the console with their own logging mechanisms. CoreStore provides its own default logging class, but you can plug-in your own favorite logger by implementing the CoreStoreLogger protocol.
Implement this protocol with your custom class then pass the instance to CoreStoreDefaults.logger:
CoreStoreDefaults.logger = MyLogger()
Doing so channels all logging calls to your logger.
Note that to keep the call stack information intact, all calls to these methods are NOT thread-managed. Therefore you have to make sure that your logger is thread-safe or you may otherwise have to dispatch your logging implementation to a serial queue.
Take special care when implementing CoreStoreLogger‘s assert(...) and abort(...) functions:
assert(...): The behavior between DEBUG and release builds, or -O and -Onone, are all left to the implementers’ responsibility. CoreStore calls CoreStoreLogger.assert(...) only for invalid but usually recoverable errors (for example, early validation failures that may cause an error thrown and handled somewhere else)
abort(...): This method is the last-chance for your app to synchronously log a fatal error within CoreStore. The app will be terminated right after this function is called (CoreStore calls fatalError() internally)
All CoreStore types have very useful (and pretty formatted!) print(...) outputs.
A couple of examples, ListMonitor:
CoreStoreError.mappingModelNotFoundError:
These are all implemented with CustomDebugStringConvertible.debugDescription, so they work with lldb’s po command as well.
Observing changes and notifications
CoreStore provides type-safe wrappers for observing managed objects:
For CoreStoreObject subclasses: Call the observe(...) method directly on the property. You’ll notice that the API itself is a bit similar to the KVO method:
For both methods, you will need to keep a reference to the returned observer for the duration of the observation.
Observe a single object’s updates
Observers of an ObjectPublisher can receive notifications if any of the object’s property changes. You can create an ObjectPublisher from the object directly:
let objectPublisher: ObjectPublisher<Person> = person.asPublisher(in: dataStack)
or by indexing a ListPublisher‘s ListSnapshot:
let listPublisher: ListPublisher<Person> = // ...
// ...
let objectPublisher = listPublisher.snapshot[indexPath]
To receive notifications, call the ObjectPublisher‘s addObserve(...) method passing the owner of the callback closure:
objectPublisher.addObserver(self) { [weak self] (objectPublisher) in
let snapshot: ObjectSnapshot<Person> = objectPublisher.snapshot
// handle changes
}
Note that the owner instance will not be retained. You may call ObjectPublisher.removeObserver(...) explicitly to stop receiving notifications, but the ObjectPublisher also discontinues sending events to deallocated observers.
The ObjectSnapshot returned from the ObjectPublisher.snapshot property returns a full-copy struct of all properties of the object. This is ideal for managing states as they are thread-safe and are not affected by further changes to the actual object. ObjectPublisher automatically updates its snapshot value to the latest state of the object.
We then need to keep an ObjectMonitor instance and register our ObjectObserver as an observer:
let person: MyPersonEntity = // ...
self.monitor = dataStack.monitorObject(person)
self.monitor.addObserver(self)
The controller will then notify our observer whenever the object’s attributes change. You can add multiple ObjectObservers to a single ObjectMonitor without any problem. This means you can just share around the ObjectMonitor instance to different screens without problem.
You can get ObjectMonitor‘s object through its object property. If the object is deleted, the object property will become nil to prevent further access.
While ObjectMonitor exposes removeObserver(...) as well, it only stores weak references of the observers and will safely unregister deallocated observers.
Observe a diffable list
Observers of a ListPublisher can receive notifications whenever its fetched result set changes. You can create a ListPublisher by fetching from the DataStack:
let listPublisher = dataStack.listPublisher(
From<Person>()
.sectionBy(\.age") { "Age \($0)" } // sections are optional
.where(\.title == "Engineer")
.orderBy(.ascending(\.lastName))
)
To receive notifications, call the ListPublisher‘s addObserve(...) method passing the owner of the callback closure:
listPublisher.addObserver(self) { [weak self] (listPublisher) in
let snapshot: ListSnapshot<Person> = listPublisher.snapshot
// handle changes
}
Note that the owner instance will not be retained. You may call ListPublisher.removeObserver(...) explicitly to stop receiving notifications, but the ListPublisher also discontinues sending events to deallocated observers.
The ListSnapshot returned from the ListPublisher.snapshot property returns a full-copy struct of all sections and NSManagedObject items in the list. This is ideal for managing states as they are thread-safe and are not affected by further changes to the result set. ListPublisher automatically updates its snapshot value to the latest state of the fetch.
Unlike ListMonitors (See ListMonitor examples below), a ListPublisher does not track detailed inserts, deletes, and moves. In return, a ListPublisher is a lot more lightweight and are designed to work well with DiffableDataSource.TableViewAdapters and DiffableDataSource.CollectionViewAdapters:
listMonitorDidChange(_:) and listMonitorDidRefetch(_:) implementations are both required. listMonitorDidChange(_:) is called whenever the ListMonitor‘s count, order, or filtered objects change. listMonitorDidRefetch(_:) is called when the ListMonitor.refetch() was executed or if the internal persistent store was changed.
ListObjectObserver: in addition to ListObserver methods, also lets you handle object inserts, updates, and deletes:
Similar to ObjectMonitor, a ListMonitor can also have multiple ListObservers registered to a single ListMonitor.
If you have noticed, the monitorList(...) method accepts Where, OrderBy, and Tweak clauses exactly like a fetch. As the list maintained by ListMonitor needs to have a deterministic order, at least the From and OrderBy clauses are required.
A ListMonitor created from monitorList(...) will maintain a single-section list. You can therefore access its contents with just an index:
let firstPerson = self.monitor[0]
If the list needs to be grouped into sections, create the ListMonitor instance with the monitorSectionedList(...) method and a SectionBy clause:
A list controller created this way will group the objects by the attribute key indicated by the SectionBy clause. One more thing to remember is that the OrderBy clause should sort the list in such a way that the SectionBy attribute would be sorted together (a requirement shared by NSFetchedResultsController.)
The SectionBy clause can also be passed a closure to transform the section name into a displayable string:
self.monitor = dataStack.monitorSectionedList(
From<MyPersonEntity>()
.sectionBy(\.age) { (sectionName) -> String? in
"\(sectionName) years old"
}
.orderBy(.ascending(\.age), .ascending(\.name))
)
This is useful when implementing a UITableViewDelegate‘s section header:
To access the objects of a sectioned list, use an IndexPath or a tuple:
let indexPath = IndexPath(row: 2, section: 1)
let person1 = self.monitor[indexPath]
let person2 = self.monitor[1, 2]
// person1 and person2 are the same object
Type-safe CoreStoreObjects
Starting CoreStore 4.0, we can now create persisted objects without depending on .xcdatamodeld Core Data files. The new CoreStoreObject subclass replaces NSManagedObject, and specially-typed properties declared on these classes will be synthesized as Core Data attributes.
class Animal: CoreStoreObject {
@Field.Stored("species")
var species: String = ""
}
class Dog: Animal {
@Field.Stored("nickname")
var nickname: String?
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Stored("name")
var name: String = ""
@Field.Relationship("pets", inverse: \Dog.$master)
var pets: Set<Dog>
}
The property names to be saved to Core Data is specified as the keyPath argument. This lets us refactor our Swift code without affecting the underlying database. For example:
class Person: CoreStoreObject {
@Field.Stored("name")
private var internalName: String = ""
// note property name is independent of the storage key name
}
Here we used the property name internalName and made it private, but the underlying key-path "name" was unchanged so our model will not trigger a data migration.
To tell the DataStack about these types, add all CoreStoreObjects’ entities to a CoreStoreSchema:
All CoreStore APIs that are usable with NSManagedObjects are also available for CoreStoreObjects. These include ListMonitors, ImportableObjects, fetching, etc.
New @Field Property Wrapper syntax
⚠️Important:@Field properties are only supported for CoreStoreObject subclasses. If you are using NSManagedObjects, you need to keep using @NSManaged for your attributes.
Starting CoreStore 7.1.0, CoreStoreObject properties may be converted to @Field Property Wrappers.
‼️ Please take note of the warnings below before converting or else the model’s hash might change.
If conversion is too risky, the current Value.Required, Value.Optional, Transformable.Required, Transformable.Optional, Relationship.ToOne, Relationship.ToManyOrdered, and Relationship.ToManyUnordered will all be supported for while so you can opt to use them as is for now.
‼️ This cannot be stressed enough, but please make sure to set your schema’s VersionLock before converting!
@Field.Stored
The @Field.Stored property wrapper is used for persisted value types. This is the replacement for “non-transient” Value.Required and Value.Optional properties.
Before
@Field.Stored
class Person: CoreStoreObject {
let title = Value.Required<String>("title", initial: "Mr.")
let nickname = Value.Optional<String>("nickname")
}
class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
@Field.Stored("nickname")
var nickname: String?
}
⚠️ Only Value.Required and Value.Optional that are NOT transient values can be converted to Field.Stored. For transient/computed properties, refer to @Field.Virtual properties in the next section.
⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model’s hash might change.
@Field.Virtual
The @Field.Virtual property wrapper is used for unsaved, computed value types. This is the replacement for “transient” Value.Required and Value.Optional properties.
Before
@Field.Virtual
class Animal: CoreStoreObject {
let speciesPlural = Value.Required<String>(
"speciesPlural",
transient: true,
customGetter: Animal.getSpeciesPlural(_:)
)
let species = Value.Required<String>("species", initial: "")
static func getSpeciesPlural(_ partialObject: PartialObject<Animal>) -> String? {
let species = partialObject.value(for: { $0.species })
return species + "s"
}
}
class Animal: CoreStoreObject {
@Field.Virtual(
"speciesPlural",
customGetter: { (object, field) in
return object.$species.value + "s"
}
)
var speciesPlural: String
@Field.Stored("species")
var species: String = ""
}
⚠️ Only Value.Required and Value.Optional that ARE transient values can be converted to Field.Virtual. For non-transient properties, refer to @Field.Stored properties in the previous section.
⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model’s hash might change.
@Field.Coded
The @Field.Coded property wrapper is used for binary-codable values. This is the new counterpart, not replacement, for Transformable.Required and Transformable.Optional properties. @Field.Coded also supports other encodings such as JSON and custom binary converters.
‼️ The current Transformable.Required and Transformable.Optional mechanism have no safe one-to-one conversion to @Field.Coded. Please use @Field.Coded only for newly added attributes.
Before
@Field.Coded
class Vehicle: CoreStoreObject {
let color = Transformable.Optional<UIColor>("color", initial: .white)
}
class Vehicle: CoreStoreObject {
@Field.Coded("color", coder: FieldCoders.NSCoding.self)
var color: UIColor? = .white
}
Built-in encoders such as FieldCoders.NSCoding, FieldCoders.Json, and FieldCoders.Plist are available, and custom encoding/decoding is also supported:
‼️Important: Any changes in the encoders/decoders are not reflected in the VersionLock, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store.
@Field.Relationship
The @Field.Relationship property wrapper is used for link relationships with other CoreStoreObjects. This is the replacement for Relationship.ToOne, Relationship.ToManyOrdered, and Relationship.ToManyUnordered properties.
The type of relationship is determined by the @Field.Relationship generic type:
Optional<T> : To-one relationship
Array<T> : To-many ordered relationship
Set<T> : To-many unordered relationship
Before
@Field.Stored
class Pet: CoreStoreObject {
let master = Relationship.ToOne<Person>("master")
}
class Person: CoreStoreObject {
let pets: Relationship.ToManyUnordered<Pet>("pets", inverse: \.$master)
}
class Pet: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Pet>
}
⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model’s hash might change.
Also note how Relationships are linked statically with the inverse: argument. All relationships are required to have an “inverse” relationship. Unfortunately, due to Swift compiler limitation we can declare the inverse: on only one of the relationship-pair.
@Field usage notes
Accessor syntax
When using key-path utilities, properties using @Field property wrappers need to use the $ syntax:
Before: From<Person>.where(\.title == "Mr.")
After: From<Person>.where(\.$title == "Mr.")
This applies to property access using ObjectPublishers and ObjectSnapshots.
Before: let name = personSnapshot.name
After: let name = personSnapshot.$name
Default values vs. Initial values
One common mistake when assigning default values to CoreStoreObject properties is to assign it a value and expect it to be evaluated whenever an object is created:
// ❌
class Person: CoreStoreObject {
@Field.Stored("identifier")
var identifier: UUID = UUID() // Wrong!
@Field.Stored("createdDate")
var createdDate: Date = Date() // Wrong!
}
This default value will be evaluated only when the DataStack sets up the schema, and all instances will end up having the same values. This syntax for “default values” are usually used only for actual reasonable constant values, or sentinel values such as "" or 0.
For actual “initial values”, @Field.Stored and @Field.Coded now supports dynamic evaluation during object creation via the dynamicInitialValue: argument:
// ✅
class Person: CoreStoreObject {
@Field.Stored("identifier", dynamicInitialValue: { UUID() })
var identifier: UUID
@Field.Stored("createdDate", dynamicInitialValue: { Date() })
var createdDate: Date
}
When using this feature, a “default value” should not be assigned (i.e. no = expression).
VersionLocks
While it is convenient to be able to declare entities only in code, it is worrying that we might accidentally change the CoreStoreObject‘s properties and break our users’ model version history. For this, the CoreStoreSchema allows us to “lock” our properties to a particular configuration. Any changes to that VersionLock will raise an assertion failure during the CoreStoreSchema initialization, so you can then look for the commit which changed the VersionLock hash.
To use VersionLocks, create the CoreStoreSchema, run the app, and look for a similar log message that is automatically printed to the console:
Copy this dictionary value and use it as the versionLock: argument of the CoreStoreSchema initializer:
Once the version lock is set, any changes in the properties or to the model will trigger an assertion failure similar to this:
Reactive Programming
RxSwift
RxSwift utilities are available through the RxCoreStore external module.
Combine
Combine publishers are available from the DataStack, ListPublisher, and ObjectPublisher‘s .reactive namespace property.
DataStack.reactive
Adding a storage through DataStack.reactive.addStorage(_:) returns a publisher that reports a MigrationProgressenum value. The .migrating value is only emitted if the storage goes through a migration. Refer to the Setting up section for details on the storage setup process itself.
dataStack.reactive
.addStorage(
SQLiteStore(fileName: "core_data.sqlite")
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (progress) in
print("\(round(progress.fractionCompleted * 100)) %") // 0.0 ~ 1.0
switch progress {
case .migrating(let storage, let nsProgress):
// ...
case .finished(let storage, let migrationRequired):
// ...
}
}
)
.store(in: &cancellables)
Transactions are also available as publishers through DataStack.reactive.perform(_:), which returns a Combine Future that emits any type returned from the closure parameter:
dataStack.reactive
.perform(
asynchronous: { (transaction) -> (inserted: Set<NSManagedObject>, deleted: Set<NSManagedObject>) in
// ...
return (
transaction.insertedObjects(),
transaction.deletedObjects()
)
}
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { value in
let inserted = dataStack.fetchExisting(value0.inserted)
let deleted = dataStack.fetchExisting(value0.deleted)
// ...
}
)
.store(in: &cancellables)
For importing convenience, ImportableObject and ImportableUniqueObjects can be imported directly through DataStack.reactive.import[Unique]Object(_:source:) and DataStack.reactive.import[Unique]Objects(_:sourceArray:) without having to create a transaction block. In this case the publisher emits objects that are already usable directly from the main queue:
ListPublishers can be used to emit ListSnapshots through Combine using ListPublisher.reactive.snapshot(emitInitialValue:). The snapshot values are emitted in the main queue:
listPublisher.reactive
.snapshot(emitInitialValue: true)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (listSnapshot) in
dataSource.apply(
listSnapshot,
animatingDifferences: true
)
}
)
.store(in: &cancellables)
ObjectPublisher.reactive
ObjectPublishers can be used to emit ObjectSnapshots through Combine using ObjectPublisher.reactive.snapshot(emitInitialValue:). The snapshot values are emitted in the main queue:
objectPublisher.reactive
.snapshot(emitInitialValue: true)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (objectSnapshot) in
tableViewCell.setObject(objectSnapshot)
}
)
.store(in: &tableViewCell.cancellables)
SwiftUI Utilities
Observing list and object changes in SwiftUI can be done through a couple of approaches. One is by creating views that autoupdates their contents, or by declaring property wrappers that trigger view updates. Both approaches are implemented almost the same internally, but this lets you be flexible depending on the structure of your custom Views.
SwiftUI Views
CoreStore provides View containers that automatically update their contents when data changes.
ListReader
A ListReader observes changes to a ListPublisher and creates its content views dynamically. The builder closure receives a ListSnapshot value that can be used to create the contents:
let people: ListPublisher<Person>
var body: some View {
List {
ListReader(self.people) { listSnapshot in
ForEach(objectIn: listSnapshot) { person in
// ...
}
}
}
.animation(.default)
}
As shown above, a typical use case is to use it together with CoreStore’s ForEach extensions.
A KeyPath can also be optionally provided to extract specific properties of the ListSnapshot:
let people: ListPublisher<Person>
var body: some View {
ListReader(self.people, keyPath: \.count) { count in
Text("Number of members: \(count)")
}
}
ObjectReader
An ObjectReader observes changes to an ObjectPublisher and creates its content views dynamically. The builder closure receives an ObjectSnapshot value that can be used to create the contents:
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(self.person) { objectSnapshot in
// ...
}
.animation(.default)
}
A KeyPath can also be optionally provided to extract specific properties of the ObjectSnapshot:
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(self.person, keyPath: \.fullName) { fullName in
Text("Name: \(fullName)")
}
}
By default, an ObjectReader does not create its views wheen the object observed is deleted from the store. In those cases, the placeholder: argument can be used to provide a custom View to display when the object is deleted:
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(
self.person,
content: { objectSnapshot in
// ...
},
placeholder: { Text("Record not found") }
)
}
SwiftUI Property Wrappers
As an alternative to ListReader and ObjectReader, CoreStore also provides property wrappers that trigger view updates when the data changes.
ListState
A @ListState property exposes a ListSnapshot value that automatically updates to the latest changes.
@ListState
var people: ListSnapshot<Person>
init(listPublisher: ListPublisher<Person>) {
self._people = .init(listPublisher)
}
var body: some View {
List {
ForEach(objectIn: self.people) { objectSnapshot in
// ...
}
}
.animation(.default)
}
As shown above, a typical use case is to use it together with CoreStore’s ForEach extensions.
If a ListPublisher instance is not available yet, the fetch can be done inline by providing the fetch clauses and the DataStack instance. By doing so the property can be declared without an initial value:
@ListState(
From<Person>()
.sectionBy(\.age)
.where(\.isMember == true)
.orderBy(.ascending(\.lastName))
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.people) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { person in
// ...
}
}
}
}
.animation(.default)
}
For other initialization variants, refer to the ListState.swift source documentations.
ObjectState
An @ObjectState property exposes an optional ObjectSnapshot value that automatically updates to the latest changes.
@ObjectState
var person: ObjectSnapshot<Person>?
init(objectPublisher: ObjectPublisher<Person>) {
self._person = .init(objectPublisher)
}
var body: some View {
HStack {
if let person = self.person {
AsyncImage(person.$avatarURL)
Text(person.$fullName)
}
else {
Text("Record removed")
}
}
}
As shown above, the property’s value will be nil if the object has been deleted, so this can be used to display placeholders if needed.
SwiftUI Extensions
For convenience, CoreStore provides extensions to the standard SwiftUI types.
ForEach
Several ForEach initializer overloads are available. Choose depending on your input data and the expected closure data. Refer to the table below (Take note of the argument labels as they are important):
Data
Example
Signature:
ForEach(_: [ObjectSnapshot<O>])
Closure:
ObjectSnapshot<O>
let array: [ObjectSnapshot<Person>]
var body: some View {
List {
ForEach(self.array) { objectSnapshot in
// ...
}
}
}
Signature:
ForEach(objectIn: ListSnapshot<O>)
Closure:
ObjectPublisher<O>
let listSnapshot: ListSnapshot<Person>
var body: some View {
List {
ForEach(objectIn: self.listSnapshot) { objectPublisher in
// ...
}
}
}
Signature:
ForEach(objectIn: [ObjectSnapshot<O>])
Closure:
ObjectPublisher<O>
let array: [ObjectSnapshot<Person>]
var body: some View {
List {
ForEach(objectIn: self.array) { objectPublisher in
// ...
}
}
}
Signature:
ForEach(sectionIn: ListSnapshot<O>)
Closure:
[ListSnapshot<O>.SectionInfo]
let listSnapshot: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.listSnapshot) { sectionInfo in
// ...
}
}
}
Signature:
ForEach(objectIn: ListSnapshot<O>.SectionInfo)
Closure:
ObjectPublisher<O>
let listSnapshot: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.listSnapshot) { sectionInfo in
ForEach(objectIn: sectionInfo) { objectPublisher in
// ...
}
}
}
}
The com.apple.CoreData.ConcurrencyDebug debug argument should be turned off for the app. CoreStore already guarantees safety for you by making the main context read-only, and by only executing transactions serially.
Install with CocoaPods
In your Podfile, add
pod 'CoreStore', '~> 9.1'
and run
pod update
This installs CoreStore as a framework. Declare import CoreStore in your swift file to use the library.
Install with Carthage
In your Cartfile, add
github "JohnEstropia/CoreStore" >= 9.1.0
and run
carthage update
This installs CoreStore as a framework. Declare import CoreStore in your swift file to use the library.
Unleashing the real power of Core Data with the elegance and safety of Swift
Dependency managers
Contact
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
From
clauseWhere
clauseOrderBy
clauseTweak
clauseSelect<T>
clauseGroupBy
clauseCoreStoreObject
s@Field
Property Wrapper syntax@Field.Stored
@Field.Virtual
@Field.Coded
@Field.Relationship
@Field
usage notesVersionLock
sDataStack.reactive
ListPublisher.reactive
ObjectPublisher.reactive
ListReader
ObjectReader
ListState
ObjectState
ForEach
TL;DR (a.k.a. sample codes)
Pure-Swift models:
(Classic
NSManagedObject
s also supported)Setting-up with progressive migration support:
Adding a store:
Starting transactions:
Fetching objects (simple):
Fetching objects (complex):
Querying values:
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
ListPublisher
s andObjectPublisher
s now have their@ListState
and@ObjectState
SwiftUI property wrappers. CombinePublisher
s are also available through theListPublisher.reactive
,ObjectPublisher.reactive
, andDataStack.reactive
namespaces.UITableViews
andUICollectionViews
now have a new ally:ListPublisher
s provide diffable snapshots that make reloading animations very easy and very safe. Say goodbye toUITableViews
andUICollectionViews
reload errors!NSManagedObjectContext
is strictly read-only, while all updates are done through serial transactions. (See Saving and processing transactions)min
,max
, etc.) and raw property values is now just as convenient. (See Fetching and querying)NSFetchedResultsController
s 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)NSManagedObject
s, it offersCoreStoreObject
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-safeCoreStoreObject
s)DataStack
the sequence of version strings (MigrationChain
s) and CoreStore will automatically use progressive migrations when needed. (See Migrations)CoreStoreLogger
protocol implementations. (See Logging and error reporting)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)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.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:NSPersistentContainer
(.xcdatamodeld file)
DataStack
NSPersistentStoreDescription
(“Configuration”s in the .xcdatamodeld file)
StorageInterface
implementations(
InMemoryStore
,SQLiteStore
)NSManagedObjectContext
BaseDataTransaction
subclasses(
SynchronousDataTransaction
,AsynchronousDataTransaction
,UnsafeDataTransaction
)A lot of Core Data wrapper libraries set up their
NSManagedObjectContext
s this way:Nesting saves from child context to the root context ensures maximum data integrity between contexts without blocking the main queue. But in reality, merging contexts is still by far faster than saving contexts. CoreStore’s
DataStack
takes the best of both worlds by treating the mainNSManagedObjectContext
as a read-only context (or “viewContext”), and only allows changes to be made within transactions on the child context: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:
This one-liner does the following:
CoreStoreDefaults.dataStack
with a defaultDataStack
NSPersistentStoreCoordinator
, the root savingNSManagedObjectContext
, and the read-only mainNSManagedObjectContext
SQLiteStore
in the “Application Support/NSPersistentStore
instance on success, or anNSError
on failureFor most cases, this configuration is enough as it is. But for more hardcore settings, refer to this extensive example:
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 theDataStack
like below and call all its instance methods directly:Notice that in our previous examples,
addStorageAndWait(_:)
andaddStorage(_:completion:)
both accept eitherInMemoryStore
, orSQLiteStore
. These implement theStorageInterface
protocol.In-memory store
The most basic
StorageInterface
concrete type is theInMemoryStore
, which just stores objects in memory. SinceInMemoryStore
s always start with a fresh empty data, they do not need any migration information.Asynchronous variant:
(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 theSQLiteStore
, which saves data in a local SQLite file.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
SQLiteStore
s can be initialized with no arguments:(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, theLocalStorage
protocol:If you have custom
NSIncrementalStore
orNSAtomicStore
subclasses, you can implement this protocol and use it similarly toSQLiteStore
.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 withCoreStoreObject
entities. (See Type-safeCoreStoreObject
s)UnsafeDataModelSchema
: a model version created with an existingNSManagedObjectModel
instance.All the
DynamicSchema
for all model versions are then collected within a singleSchemaHistory
instance, which is then handed to theDataStack
. Here are some common use cases:Multiple model versions grouped in a .xcdatamodeld file (Core Data standard method)
CoreStoreSchema
-based model version (No .xcdatamodeld file needed) (For more details, see also Type-safeCoreStoreObject
s)Models in a .xcdatamodeld file during past app versions, but migrated to the new
CoreStoreSchema
methodCoreStoreSchema
-based model versions with progressive migrationStarting 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: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:The
completion
block reports aSetupResult
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
. Ifnil
, no migrations are needed, thus progress reporting is unnecessary as well. If notnil
, 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: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
‘scurrentModelVersion
. 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 aMigrationChain
. This is typically passed to theDataStack
initializer and will be applied to all stores added to theDataStack
withaddSQLiteStore(...)
and its variants:The most common usage is to pass in the model version (.xcdatamodeld version names for
NSManagedObject
s, or themodelName
forCoreStoreSchema
s) 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:
This allows for different migration paths depending on the starting version. The example above resolves to the following paths:
Initializing with empty values (either
nil
,[]
, or[:]
) instructs theDataStack
to disable progressive migrations and revert to the default migration behavior (i.e. use the .xcdatamodeld‘s current version as the final version):The
MigrationChain
is validated when passed to theDataStack
and unless it is empty, will raise an assertion if any of the following conditions are met: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 calladdStorageAndWait(_:)
oraddStorage(_:completion:)
:requiredMigrationsForStorage(_:)
returns an array ofMigrationType
s, where each item in the array may be either of the following values:Each
MigrationType
indicates the migration type for each step in theMigrationChain
. 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 withCoreStoreObject
s as well, but may also be used withNSManagedObject
s.XcodeSchemaMappingProvider
: A mapping provider which loads entity mappings from .xcmappingmodel files in a specifiedBundle
.InferredSchemaMappingProvider
: The default mapping provider which tries to infer model migration between twoDynamicSchema
versions either by searching all .xcmappingmodel files fromBundle.allBundles
, or by relying on lightweight migration if possible.These mapping providers conform to
SchemaMappingProvider
and can be passed toSQLiteStore
‘s initializer:For version migrations present in the
DataStack
‘sMigrationChain
but not handled by any of theSQLiteStore
‘smigrationMappingProviders
array, CoreStore will automatically try to useInferredSchemaMappingProvider
as fallback. Finally if theInferredSchemaMappingProvider
could not resolve any mapping, the migration will fail and theDataStack.addStorage(...)
method will report the failure.For
CustomSchemaMappingProvider
, more granular updates are supported through the dynamic objectsUnsafeSourceObject
andUnsafeDestinationObject
. The example below allows the migration to conditionally ignore some objects:The
UnsafeSourceObject
is a read-only proxy for an object existing in the source model version. TheUnsafeDestinationObject
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 fromDataStack
instances:Transaction closures automatically save changes once the closures completes. To cancel and rollback a transaction, throw a
CoreStoreError.userCancelled
from inside the closure by callingtry transaction.cancel()
: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:The success and failure can also be declared as separate handlers:
Transactions created from
perform(asynchronous:...)
are instances ofAsynchronousDataTransaction
.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:transaction
above is aSynchronousDataTransaction
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 asListMonitor
s 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 thewaitForAllObservers:
parameter tofalse
. Doing so tells theSynchronousDataTransaction
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: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:
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 anInto
clause which specifies the entity for the object you want to create:While the syntax is straightforward, CoreStore does not just naively insert a new object. This single line does the following:
create(...)
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:
or if the persistent store is the auto-generated “Default” configuration, specify
nil
: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:
To update an existing object, fetch the object’s instance from the transaction:
(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: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:
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):
or several objects at once:
If you do not have references yet to the objects to be deleted, transactions have a
deleteAll(...)
method you can pass a query to:Passing objects safely
Always remember that the
DataStack
and individual transactions manage differentNSManagedObjectContext
s so you cannot just use objects between them. That’s why transactions have anedit(...)
method:But
CoreStore
,DataStack
andBaseDataTransaction
have a very flexiblefetchExisting(...)
method that you can pass instances back and forth with:fetchExisting(...)
also works with multipleNSManagedObject
s,CoreStoreObject
s, or withNSManagedObjectID
s: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:
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(...)
orimportUniqueObject(...)
throughBaseDataTransaction
subclasses:To support data import for an entity, implement either
ImportableObject
orImportableUniqueObject
on theNSManagedObject
orCoreStoreObject
subclass:ImportableObject
: Use this protocol if the object have no inherent uniqueness and new objects should always be added when callingimportObject(...)
.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 callingimportUniqueObject(...)
.Both protocols require implementers to specify an
ImportSource
which can be set to any type that the object can extract data from: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:First, set
ImportSource
to the expected type of the data source:This lets us call
importObject(_:source:)
with any[String: Any]
type as the argument tosource
:The actual extraction and assignment of values should be implemented in the
didInsert(from:in:)
method of theImportableObject
protocol:Transactions also let you import multiple objects at once using the
importObjects(_:sourceArray:)
method:Doing so tells the transaction to iterate through the array of import sources and calls
shouldInsert(from:in:)
on theImportableObject
to determine which instances should be created. You can do validations and returnfalse
fromshouldInsert(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 withindidInsert(from:in:)
:Doing so can let you abandon an invalid transaction immediately:
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:Notice that it has the same insert methods as
ImportableObject
, with additional methods for updates and for specifying the unique ID:For
ImportableUniqueObject
, the extraction and assignment of values should be implemented from theupdate(from:in:)
method. ThedidInsert(from:in:)
by default callsupdate(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:or multiple objects at once with the
importUniqueObjects(...)
method:As with
ImportableObject
, you can control whether to skip importing an object by implementingshouldInsert(from:in:)
andshouldUpdate(from:in:)
, or to cancel all objects bythrow
ing an error from theuniqueID(from:in:)
,didInsert(from:in:)
orupdate(from:in:)
methods.Fetching and Querying
Before we dive in, be aware that CoreStore distinguishes between fetching and querying:
commit()
.) Use fetches when:NSManagedObject
orCoreStoreObject
instancesNSString
s,NSNumber
s,Int
s,NSDate
s, anNSDictionary
of key-values, or any type that conform toQueryableAttributeType
. (See QueryableAttributeType.swift for a list of built-in types)From
clauseThe search conditions for fetches and queries are specified using clauses. All fetches and queries require a
From
clause that indicates the target entity type:people
in the example above will be of type[MyPersonEntity]
. TheFrom<MyPersonEntity>()
clause indicates a fetch to all persistent stores thatMyPersonEntity
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:or if the persistent store is the auto-generated “Default” configuration, specify
nil
: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 aDataStack
instance, or from aBaseDataTransaction
instance. All of the methods below accept the same parameters: a requiredFrom
clause, and an optional series ofWhere
,OrderBy
, and/orTweak
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 ofNSManagedObjectID
s for all objects that match the criteria.fetchObjectID(...)
- returns theNSManagedObjectID
s 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
clauseThe
Where
clause is CoreStore’sNSPredicate
wrapper. It specifies the search filter to use when fetching (or querying). It implements all initializers thatNSPredicate
does (except for-predicateWithBlock:
, which Core Data does not support):If you do have an existing
NSPredicate
instance already, you can pass that toWhere
as well: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 theWhere
clause expression:Where
clauses also implement the&&
,||
, and!
logic operators, so you can provide logical conditions without writing too muchAND
,OR
, andNOT
strings:If you do not provide a
Where
clause, all objects that belong to the specifiedFrom
will be returned.OrderBy
clauseThe
OrderBy
clause is CoreStore’sNSSortDescriptor
wrapper. Use it to specify attribute keys in which to sort the fetch (or query) results with.As seen above,
OrderBy
accepts a list ofSortKey
enumeration values, which can be either.ascending
or.descending
. As withWhere
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 theOrderBy
clause expression:You can use the
+
and+=
operator to appendOrderBy
s together. This is useful when sorting conditionally:Tweak
clauseThe
Tweak
clause lets you, uh, tweak the fetch (or query).Tweak
exposes theNSFetchRequest
in a closure where you can make changes to its properties:Tweak
also supports Fetch Chain builders: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 theNSFetchRequest
, note that CoreStore already preconfigured thatNSFetchRequest
to suitable defaults. Only useTweak
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 probably know how painful it is to setup a query for raw values and aggregate values. CoreStore makes this easy by exposing the 2 methods below:queryValue(...)
- returns a single raw value for an attribute or for an aggregate value. If there are multiple results,queryValue(...)
only returns the first item.queryAttributes(...)
- returns an array of dictionaries containing attribute keys with their corresponding values.Both methods above accept the same parameters: a required
From
clause, a requiredSelect<T>
clause, and an optional series ofWhere
,OrderBy
,GroupBy
, and/orTweak
clauses.Setting up the
From
,Where
,OrderBy
, andTweak
clauses is similar to how you would when fetching. For querying, you also need to know how to use theSelect<T>
andGroupBy
clauses.Select<T>
clauseThe
Select<T>
clause specifies the target attribute/aggregate key, as well as the expected return type:The example above queries the “age” property for the first object that matches the
Where
condition.johnsAge
will be bound to typeInt?
, as indicated by theSelect<Int>
generic type. ForqueryValue(...)
, types that conform toQueryableAttributeType
are allowed as the return type (and therefore as the generic type forSelect<T>
).For
queryAttributes(...)
, onlyNSDictionary
is valid forSelect
, thus you are allowed to omit the generic type:query methods also support Query Chain builders. We can also use Swift’s Smart KeyPaths to use in the expressions:
If you only need a value for a particular attribute, you can just specify the key name (like we did with
Select<Int>("age")
), but several aggregate functions can also be used as parameter toSelect
:.average(...)
.count(...)
.maximum(...)
.minimum(...)
.sum(...)
For
queryAttributes(...)
which returns an array of dictionaries, you can specify multiple attributes/aggregates toSelect
:personJSON
will then have the value:You can also include an aggregate as well:
which returns:
The
"count(friends)"
key name was automatically used by CoreStore, but you can specify your own key alias if you need:which now returns:
GroupBy
clauseThe
GroupBy
clause lets you group results by a specified attribute/aggregate. This is useful only forqueryAttributes(...)
sincequeryValue(...)
just returns the first value.GroupBy
clauses are also generic types and support Query Chain builders. We can also use Swift’s Smart KeyPaths to use in the expressions:this returns dictionaries that shows the count for each
"age"
:Logging and error reporting
One unfortunate thing when using some third-party libraries is that they usually pollute the console with their own logging mechanisms. CoreStore provides its own default logging class, but you can plug-in your own favorite logger by implementing the
CoreStoreLogger
protocol.Implement this protocol with your custom class then pass the instance to
CoreStoreDefaults.logger
:Doing so channels all logging calls to your logger.
Note that to keep the call stack information intact, all calls to these methods are NOT thread-managed. Therefore you have to make sure that your logger is thread-safe or you may otherwise have to dispatch your logging implementation to a serial queue.
Take special care when implementing
CoreStoreLogger
‘sassert(...)
andabort(...)
functions:assert(...)
: The behavior betweenDEBUG
and release builds, or-O
and-Onone
, are all left to the implementers’ responsibility. CoreStore callsCoreStoreLogger.assert(...)
only for invalid but usually recoverable errors (for example, early validation failures that may cause an error thrown and handled somewhere else)abort(...)
: This method is the last-chance for your app to synchronously log a fatal error within CoreStore. The app will be terminated right after this function is called (CoreStore callsfatalError()
internally)All CoreStore types have very useful (and pretty formatted!)
print(...)
outputs. A couple of examples,ListMonitor
:CoreStoreError.mappingModelNotFoundError
:These are all implemented with
CustomDebugStringConvertible.debugDescription
, so they work with lldb’spo
command as well.Observing changes and notifications
CoreStore provides type-safe wrappers for observing managed objects:
Observe a single property
To get notifications for single property changes in an object, there are two methods depending on the object’s base class.
For
NSManagedObject
subclasses: Use the standard KVO method:For
CoreStoreObject
subclasses: Call theobserve(...)
method directly on the property. You’ll notice that the API itself is a bit similar to the KVO method:For both methods, you will need to keep a reference to the returned
observer
for the duration of the observation.Observe a single object’s updates
Observers of an
ObjectPublisher
can receive notifications if any of the object’s property changes. You can create anObjectPublisher
from the object directly:or by indexing a
ListPublisher
‘sListSnapshot
:(See
ListPublisher
examples below)To receive notifications, call the
ObjectPublisher
‘saddObserve(...)
method passing the owner of the callback closure:Note that the owner instance will not be retained. You may call
ObjectPublisher.removeObserver(...)
explicitly to stop receiving notifications, but theObjectPublisher
also discontinues sending events to deallocated observers.The
ObjectSnapshot
returned from theObjectPublisher.snapshot
property returns a full-copystruct
of all properties of the object. This is ideal for managing states as they are thread-safe and are not affected by further changes to the actual object.ObjectPublisher
automatically updates itssnapshot
value to the latest state of the object.(A reactive-programming variant of this method is explained in detail in the section on
ObjectPublisher
Combine publishers)Observe a single object’s per-property updates
If you need to track specifically which properties change in an object, implement the
ObjectObserver
protocol and specify theEntityType
:We then need to keep an
ObjectMonitor
instance and register ourObjectObserver
as an observer:The controller will then notify our observer whenever the object’s attributes change. You can add multiple
ObjectObserver
s to a singleObjectMonitor
without any problem. This means you can just share around theObjectMonitor
instance to different screens without problem.You can get
ObjectMonitor
‘s object through itsobject
property. If the object is deleted, theobject
property will becomenil
to prevent further access.While
ObjectMonitor
exposesremoveObserver(...)
as well, it only storesweak
references of the observers and will safely unregister deallocated observers.Observe a diffable list
Observers of a
ListPublisher
can receive notifications whenever its fetched result set changes. You can create aListPublisher
by fetching from theDataStack
:To receive notifications, call the
ListPublisher
‘saddObserve(...)
method passing the owner of the callback closure:Note that the owner instance will not be retained. You may call
ListPublisher.removeObserver(...)
explicitly to stop receiving notifications, but theListPublisher
also discontinues sending events to deallocated observers.The
ListSnapshot
returned from theListPublisher.snapshot
property returns a full-copystruct
of all sections andNSManagedObject
items in the list. This is ideal for managing states as they are thread-safe and are not affected by further changes to the result set.ListPublisher
automatically updates itssnapshot
value to the latest state of the fetch.(A reactive-programming variant of this method is explained in detail in the section on
ListPublisher
Combine publishers)Unlike
ListMonitor
s (SeeListMonitor
examples below), aListPublisher
does not track detailed inserts, deletes, and moves. In return, aListPublisher
is a lot more lightweight and are designed to work well withDiffableDataSource.TableViewAdapter
s andDiffableDataSource.CollectionViewAdapter
s:Observe detailed list changes
If you need to track each object’s inserts, deletes, moves, and updates, implement one of the
ListObserver
protocols and specify theEntityType
:Including
ListObserver
, there are 3 observer protocols you can implement depending on how detailed you need to handle a change notification:ListObserver
: lets you handle these callback methods:listMonitorDidChange(_:)
andlistMonitorDidRefetch(_:)
implementations are both required.listMonitorDidChange(_:)
is called whenever theListMonitor
‘s count, order, or filtered objects change.listMonitorDidRefetch(_:)
is called when theListMonitor.refetch()
was executed or if the internal persistent store was changed.ListObjectObserver
: in addition toListObserver
methods, also lets you handle object inserts, updates, and deletes:ListSectionObserver
: in addition toListObjectObserver
methods, also lets you handle section inserts and deletes:We then need to create a
ListMonitor
instance and register ourListObserver
as an observer:Similar to
ObjectMonitor
, aListMonitor
can also have multipleListObserver
s registered to a singleListMonitor
.If you have noticed, the
monitorList(...)
method acceptsWhere
,OrderBy
, andTweak
clauses exactly like a fetch. As the list maintained byListMonitor
needs to have a deterministic order, at least theFrom
andOrderBy
clauses are required.A
ListMonitor
created frommonitorList(...)
will maintain a single-section list. You can therefore access its contents with just an index:If the list needs to be grouped into sections, create the
ListMonitor
instance with themonitorSectionedList(...)
method and aSectionBy
clause:A list controller created this way will group the objects by the attribute key indicated by the
SectionBy
clause. One more thing to remember is that theOrderBy
clause should sort the list in such a way that theSectionBy
attribute would be sorted together (a requirement shared byNSFetchedResultsController
.)The
SectionBy
clause can also be passed a closure to transform the section name into a displayable string:This is useful when implementing a
UITableViewDelegate
‘s section header:To access the objects of a sectioned list, use an
IndexPath
or a tuple:Type-safe
CoreStoreObject
sStarting CoreStore 4.0, we can now create persisted objects without depending on .xcdatamodeld Core Data files. The new
CoreStoreObject
subclass replacesNSManagedObject
, and specially-typed properties declared on these classes will be synthesized as Core Data attributes.The property names to be saved to Core Data is specified as the
keyPath
argument. This lets us refactor our Swift code without affecting the underlying database. For example:Here we used the property name
internalName
and made itprivate
, but the underlying key-path"name"
was unchanged so our model will not trigger a data migration.To tell the
DataStack
about these types, add allCoreStoreObject
s’ entities to aCoreStoreSchema
:And that’s all CoreStore needs to build the model; we don’t need .xcdatamodeld files anymore.
In addition,
@Field
properties can be used to create type-safe key-path stringsas well as
Where
andOrderBy
clausesAll CoreStore APIs that are usable with
NSManagedObject
s are also available forCoreStoreObject
s. These includeListMonitor
s,ImportableObject
s, fetching, etc.New
@Field
Property Wrapper syntaxStarting CoreStore 7.1.0,
CoreStoreObject
properties may be converted to@Field
Property Wrappers.If conversion is too risky, the current
Value.Required
,Value.Optional
,Transformable.Required
,Transformable.Optional
,Relationship.ToOne
,Relationship.ToManyOrdered
, andRelationship.ToManyUnordered
will all be supported for while so you can opt to use them as is for now.@Field.Stored
The
@Field.Stored
property wrapper is used for persisted value types. This is the replacement for “non-transient”Value.Required
andValue.Optional
properties.@Field.Virtual
The
@Field.Virtual
property wrapper is used for unsaved, computed value types. This is the replacement for “transient”Value.Required
andValue.Optional
properties.@Field.Coded
The
@Field.Coded
property wrapper is used for binary-codable values. This is the new counterpart, not replacement, forTransformable.Required
andTransformable.Optional
properties.@Field.Coded
also supports other encodings such as JSON and custom binary converters.Built-in encoders such as
FieldCoders.NSCoding
,FieldCoders.Json
, andFieldCoders.Plist
are available, and custom encoding/decoding is also supported:@Field.Relationship
The
@Field.Relationship
property wrapper is used for link relationships with otherCoreStoreObject
s. This is the replacement forRelationship.ToOne
,Relationship.ToManyOrdered
, andRelationship.ToManyUnordered
properties.The type of relationship is determined by the
@Field.Relationship
generic type:Optional<T>
: To-one relationshipArray<T>
: To-many ordered relationshipSet<T>
: To-many unordered relationshipAlso note how
Relationship
s are linked statically with theinverse:
argument. All relationships are required to have an “inverse” relationship. Unfortunately, due to Swift compiler limitation we can declare theinverse:
on only one of the relationship-pair.@Field
usage notesAccessor syntax
When using key-path utilities, properties using
@Field
property wrappers need to use the$
syntax:From<Person>.where(\.title == "Mr.")
From<Person>.where(\.$title == "Mr.")
This applies to property access using
ObjectPublisher
s andObjectSnapshot
s.let name = personSnapshot.name
let name = personSnapshot.$name
Default values vs. Initial values
One common mistake when assigning default values to
CoreStoreObject
properties is to assign it a value and expect it to be evaluated whenever an object is created:This default value will be evaluated only when the
DataStack
sets up the schema, and all instances will end up having the same values. This syntax for “default values” are usually used only for actual reasonable constant values, or sentinel values such as""
or0
.For actual “initial values”,
@Field.Stored
and@Field.Coded
now supports dynamic evaluation during object creation via thedynamicInitialValue:
argument:When using this feature, a “default value” should not be assigned (i.e. no
=
expression).VersionLock
sWhile it is convenient to be able to declare entities only in code, it is worrying that we might accidentally change the
CoreStoreObject
‘s properties and break our users’ model version history. For this, theCoreStoreSchema
allows us to “lock” our properties to a particular configuration. Any changes to thatVersionLock
will raise an assertion failure during theCoreStoreSchema
initialization, so you can then look for the commit which changed theVersionLock
hash.To use
VersionLock
s, create theCoreStoreSchema
, run the app, and look for a similar log message that is automatically printed to the console:Copy this dictionary value and use it as the
versionLock:
argument of theCoreStoreSchema
initializer:You can also get this hash after the
DataStack
has been fully set up by printing to the console:Once the version lock is set, any changes in the properties or to the model will trigger an assertion failure similar to this:
Reactive Programming
RxSwift
RxSwift utilities are available through the RxCoreStore external module.
Combine
Combine publishers are available from the
DataStack
,ListPublisher
, andObjectPublisher
‘s.reactive
namespace property.DataStack.reactive
Adding a storage through
DataStack.reactive.addStorage(_:)
returns a publisher that reports aMigrationProgress
enum
value. The.migrating
value is only emitted if the storage goes through a migration. Refer to the Setting up section for details on the storage setup process itself.Transactions are also available as publishers through
DataStack.reactive.perform(_:)
, which returns a CombineFuture
that emits any type returned from the closure parameter:For importing convenience,
ImportableObject
andImportableUniqueObjects
can be imported directly throughDataStack.reactive.import[Unique]Object(_:source:)
andDataStack.reactive.import[Unique]Objects(_:sourceArray:)
without having to create a transaction block. In this case the publisher emits objects that are already usable directly from the main queue:ListPublisher.reactive
ListPublisher
s can be used to emitListSnapshot
s through Combine usingListPublisher.reactive.snapshot(emitInitialValue:)
. The snapshot values are emitted in the main queue:ObjectPublisher.reactive
ObjectPublisher
s can be used to emitObjectSnapshot
s through Combine usingObjectPublisher.reactive.snapshot(emitInitialValue:)
. The snapshot values are emitted in the main queue:SwiftUI Utilities
Observing list and object changes in SwiftUI can be done through a couple of approaches. One is by creating views that autoupdates their contents, or by declaring property wrappers that trigger view updates. Both approaches are implemented almost the same internally, but this lets you be flexible depending on the structure of your custom
View
s.SwiftUI Views
CoreStore provides
View
containers that automatically update their contents when data changes.ListReader
A
ListReader
observes changes to aListPublisher
and creates its content views dynamically. The builder closure receives aListSnapshot
value that can be used to create the contents:As shown above, a typical use case is to use it together with CoreStore’s
ForEach
extensions.A
KeyPath
can also be optionally provided to extract specific properties of theListSnapshot
:ObjectReader
An
ObjectReader
observes changes to anObjectPublisher
and creates its content views dynamically. The builder closure receives anObjectSnapshot
value that can be used to create the contents:A
KeyPath
can also be optionally provided to extract specific properties of theObjectSnapshot
:By default, an
ObjectReader
does not create its views wheen the object observed is deleted from the store. In those cases, theplaceholder:
argument can be used to provide a customView
to display when the object is deleted:SwiftUI Property Wrappers
As an alternative to
ListReader
andObjectReader
, CoreStore also provides property wrappers that trigger view updates when the data changes.ListState
A
@ListState
property exposes aListSnapshot
value that automatically updates to the latest changes.As shown above, a typical use case is to use it together with CoreStore’s
ForEach
extensions.If a
ListPublisher
instance is not available yet, the fetch can be done inline by providing the fetch clauses and theDataStack
instance. By doing so the property can be declared without an initial value:For other initialization variants, refer to the ListState.swift source documentations.
ObjectState
An
@ObjectState
property exposes an optionalObjectSnapshot
value that automatically updates to the latest changes.As shown above, the property’s value will be
nil
if the object has been deleted, so this can be used to display placeholders if needed.SwiftUI Extensions
For convenience, CoreStore provides extensions to the standard SwiftUI types.
ForEach
Several
ForEach
initializer overloads are available. Choose depending on your input data and the expected closure data. Refer to the table below (Take note of the argument labels as they are important):Roadmap
Prototyping stage
Under consideration
Installation
com.apple.CoreData.ConcurrencyDebug
debug argument should be turned off for the app. CoreStore already guarantees safety for you by making the main context read-only, and by only executing transactions serially.Install with CocoaPods
In your
Podfile
, addand run
This installs CoreStore as a framework. Declare
import CoreStore
in your swift file to use the library.Install with Carthage
In your
Cartfile
, addand run
This installs CoreStore as a framework. Declare
import CoreStore
in your swift file to use the library.Install with Swift Package Manager:
Declare
import CoreStore
in your swift file to use the library.Install as Git Submodule
Drag and drop CoreStore.xcodeproj to your project.
Install through Xcode’s Swift Package Manager
From the File - Swift Packages - Add Package Dependency… menu, search for
where
JohnEstropia
is the Owner (forks may appear as well). Then add to your projectChangesets
For the full Changelog, refer to the Releases page.
Contact
You can reach me on Twitter @JohnEstropia
or join our Slack team at swift-corestore.slack.com
日本語の対応も可能なので是非!
Who uses CoreStore?
I’d love to hear about apps using CoreStore. Send me a message and I’ll welcome any feedback!
License
CoreStore is released under an MIT license. See the LICENSE file for more information