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

Overview

CoreStore

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

Build Status Last Commit Platform License

Dependency managers
Cocoapods compatible Carthage compatible Swift Package Manager compatible

Contact
Join us on Slack! Reach me on Twitter!

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

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

Pure-Swift models:

} ">
class Person: CoreStoreObject {
    @Field.Stored("name")
    var name: String = ""
    
    @Field.Relationship("pets", inverse: \Dog.$master)
    var pets: Set
}

(Classic NSManagedObjects also supported)

Setting-up with progressive migration support:

dataStack = DataStack(
    xcodeModelName: "MyStore",
    migrationChain: ["MyStore", "MyStoreV2", "MyStoreV3"]
)

Adding a store:

Void in // ... } ) ">
dataStack.addStorage(
    SQLiteStore(fileName: "MyStore.sqlite"),
    completion: { (result) -> Void in
        // ...
    }
)

Starting transactions:

Void in switch result { case .success: print("success!") case .failure(let error): print(error) } } ) ">
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)
  • 🎗 Objective-C support! Is your project transitioning from Objective-C to Swift but still can't quite fully convert some huge classes to Swift yet? CoreStore adjusts to the ever-increasing Swift adoption. While still written in pure Swift, all CoreStore types have their corresponding Objective-C-visible "bridging classes". (See Objective-C support)
  • 🛡 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)
StorageInterface implementations
(InMemoryStore, SQLiteStore)
NSManagedObjectContext BaseDataTransaction subclasses
(SynchronousDataTransaction, AsynchronousDataTransaction, UnsafeDataTransaction)

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

nested contexts

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:

nested contexts and merge hybrid

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:

try CoreStoreDefaults.dataStack.addStorageAndWait()

This one-liner does the following:

  • Triggers the lazy-initialization of CoreStoreDefaults.dataStack with a default DataStack
  • Sets up the stack's NSPersistentStoreCoordinator, the root saving NSManagedObjectContext, and the read-only main NSManagedObjectContext
  • Adds an SQLiteStore in the "Application Support/" 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:

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 ">
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 xcode configurations screenshot

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:

()) print(objects) } } ">
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
    )
)

Asynchronous variant:

try dataStack.addStorage(
    InMemoryStore(
        configuration: "Config2
    ),
    completion: { storage in
        // ...
    }
)

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

CoreStoreDefaults.dataStack = DataStack(
    xcodeModelName: "MyModel",
    bundle: Bundle.main,
    migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"]
)

CoreStoreSchema-based model version (No .xcdatamodeld file needed) (For more details, see also Type-safe CoreStoreObjects)

("Animal", isAbstract: true), Entity("Dog"), Entity("Person") ] ) ) ">
class Animal: CoreStoreObject {
    // ...
}
class Dog: Animal {
    // ...
}
class Person: CoreStoreObject {
    // ...
}

CoreStoreDefaults.dataStack = DataStack(
    CoreStoreSchema(
        modelVersion: "V1",
        entities: [
            Entity<Animal>("Animal", isAbstract: true),
            Entity<Dog>("Dog"),
            Entity<Person>("Person")
        ]
    )
)

Models in a .xcdatamodeld file during past app versions, but migrated to the new CoreStoreSchema method

("Animal", isAbstract: true), Entity("Dog"), Entity("Person") ] ) CoreStoreDefaults.dataStack = DataStack( schemaHistory: SchemaHistory( legacySchema + [newSchema], migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4", "V1"] ) ) ">
class Animal: CoreStoreObject {
    // ...
}
class Dog: Animal {
    // ...
}
class Person: CoreStoreObject {
    // ...
}

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

CoreStoreSchema-based model versions with progressive migration

("Animal", isAbstract: true), Entity("Dog"), Entity("Person") ] ), CoreStoreSchema( modelVersion: "V2", entities: [ Entity("Animal", isAbstract: true), Entity("Dog"), Entity("Person") ] ), migrationChain: ["V1", "V2"] ) ">
typealias Animal = V2.Animal
typealias Dog = V2.Dog
typealias Person = V2.Person
enum V2 {
    class Animal: CoreStoreObject {
        // ...
    }
    class Dog: Animal {
        // ...
    }
    class Person: CoreStoreObject {
        // ...
    }
}
enum V1 {
    class Animal: CoreStoreObject {
        // ...
    }
    class Dog: Animal {
        // ...
    }
    class Person: CoreStoreObject {
        // ...
    }
}

CoreStoreDefaults.dataStack = DataStack(
    CoreStoreSchema(
        modelVersion: "V1",
        entities: [
            Entity<V1.Animal>("Animal", isAbstract: true),
            Entity<V1.Dog>("Dog"),
            Entity<V1.Person>("Person")
        ]
    ),
    CoreStoreSchema(
        modelVersion: "V2",
        entities: [
            Entity<V2.Animal>("Animal", isAbstract: true),
            Entity<V2.Dog>("Dog"),
            Entity<V2.Person>("Person")
        ]
    ),
    migrationChain: ["V1", "V2"]
)

Starting migrations

We have seen addStorageAndWait(...) used to initialize our persistent store. As the method name's ~AndWait suffix suggests though, this method blocks so it should not do long tasks such as data migrations. In fact CoreStore will only attempt a synchronous lightweight migration if you explicitly provide the .allowSynchronousLightweightMigration option:

try dataStack.addStorageAndWait(
    SQLiteStore(
        fileURL: sqliteFileURL,
        localStorageOptions: .allowSynchronousLightweightMigration
    )
}

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:

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

migrationProgress?.setProgressHandler { [weak self] (progress) -> Void in
    self?.progressView?.setProgress(Float(progress.fractionCompleted), animated: true)
    self?.percentLabel?.text = progress.localizedDescription // "50% completed"
    self?.stepLabel?.text = progress.localizedAdditionalDescription // "0 of 2"
}

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:

let dataStack = DataStack(migrationChain: [
    "MyAppModel": "MyAppModelV3",
    "MyAppModelV2": "MyAppModelV4",
    "MyAppModelV3": "MyAppModelV4"
])

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

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

Void in // ... } ) ">
let dataStack = DataStack(migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"])
_ = try dataStack.addStorage(
    SQLiteStore(
        fileName: "MyStore.sqlite",
        migrationMappingProviders: [
            XcodeSchemaMappingProvider(from: "V1", to: "V2", mappingModelBundle: Bundle.main),
            CustomSchemaMappingProvider(from: "V2", to: "V3", entityMappings: [.deleteEntity("Person") ])
        ]
    ),
    completion: { (result) -> Void in
        // ...
    }
)

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:

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

Void in print(error) } ) ">
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:

.where(\.name == "John")) // newPerson may be nil! // The DataStack may have not yet received the update notification. ">
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 }
)

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

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:

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        try transaction.deleteAll(
            From<MyPersonEntity>()
                .where(\.age > 30)
        )
    },
    completion: { _ in }
)

Passing objects safely

Always remember that the DataStack and individual transactions manage different NSManagedObjectContexts so you cannot just use objects between them. That's why transactions have an edit(...) method:

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:

let json: [String: Any] = // ...
person.name = json["name"] as? NSString
person.age = json["age"] as? NSNumber
// ...

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:

func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws {
    self.name = source["name"] as? NSString
    self.age = source["age"] as? NSNumber
    // ...
}

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
        )
        // ...
    },
    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:):

func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws {
    self.name = source["name"] as? NSString
    self.age = source["age"] as? NSNumber
    // ...
    if self.name == nil {
        throw Errors.InvalidNameError
    }
}

Doing so can let you abandon an invalid transaction immediately:

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let jsonArray: [[String: Any]] = // ...

        try transaction.importObjects(
            Into<MyPersonEntity>(),
            sourceArray: jsonArray
        )
    },
    success: {
        // ...
    },
    failure: { (error) in
        switch error {
        case Errors.InvalidNameError: print("Invalid name")
        // ...
        }
    }
)

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:

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

%d", "age", 30) // string format initializer ) people = try dataStack.fetchAll( From(), Where(true) // boolean initializer ) ">
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:

%d", 30), OrderBy(.ascending("surname")), Tweak { (fetchRequest) -> Void in fetchRequest.includesPendingChanges = false fetchRequest.returnsObjectsAsFaults = false fetchRequest.includesSubentities = false } ) ">
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
    }
)

Tweak also supports Fetch Chain builders:

var people = try dataStack.fetchAll(
    From<MyPersonEntity>(),
        .where(\.age > 30)
        .orderBy(.ascending(\.surname))
        .tweak {
            $0.includesPendingChanges = false
            $0.returnsObjectsAsFaults = false
            $0.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 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 and GroupBy clauses.

Select clause

The Select clause specifies the target attribute/aggregate key, as well as the expected return type:

("name == %@", "John Smith") ) ">
let johnsAge = try dataStack.queryValue(
    From<MyPersonEntity>(),
    Select<Int>("age"),
    Where<MyPersonEntity>("name == %@", "John Smith")
)

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 generic type. For queryValue(...), types that conform to QueryableAttributeType are allowed as the return type (and therefore as the generic type for Select).

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

personJSON will then have the value:

[
    [
        "name": "John Smith",
        "age": 30
    ],
    [
        "name": "Jane Doe",
        "age": 22
    ]
]

You can also include an aggregate as well:

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("name", .count("friends"))
)

which returns:

[
    [
        "name": "John Smith",
        "count(friends)": 42
    ],
    [
        "name": "Jane Doe",
        "count(friends)": 231
    ]
]

The "count(friends)" key name was automatically used by CoreStore, but you can specify your own key alias if you need:

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("name", .count("friends", as: "friendsCount"))
)

which now returns:

[
    [
        "name": "John Smith",
        "friendsCount": 42
    ],
    [
        "name": "Jane Doe",
        "friendsCount": 231
    ]
]

GroupBy clause

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.

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("age", .count("age", as: "count")),
    GroupBy("age")
)

GroupBy clauses are also generic types and support Query Chain builders. We can also use Swift's Smart KeyPaths to use in the expressions:

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>()
        .select(.attribute(\.age), .count(\.age, as: "count"))
        .groupBy(\.age)
)

this returns dictionaries that shows the count for each "age":

[
    [
        "age": 42,
        "count": 1
    ],
    [
        "age": 22,
        "count": 1
    ]
]

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.

public protocol CoreStoreLogger {
    func log(level level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
    func log(error error: CoreStoreError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
    func assert(@autoclosure condition: () -> Bool, @autoclosure message: () -> String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
    func abort(message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
}

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:

screen shot 2016-07-10 at 22 56 44

CoreStoreError.mappingModelNotFoundError:

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:

🆕 ObjectPublisher ObjectMonitor 🆕 ListPublisher ListMonitor
Number of objects 1 1 N N
Allows multiple observers
Emits fine-grained changes
Emits DiffableDataSource snapshots
Delegate methods
Closure callback
SwiftUI support

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:
let observer = person.observe(\.age, options: [.new]) { (person, change)
    print("Happy \(change.newValue)th birthday!")
}
  • 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:
let observer = person.age.observe(options: [.new]) { (person, change)
    print("Happy \(change.newValue)th birthday!")
}

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.asPublisher(in: dataStack)

or by indexing a ListPublisher's ListSnapshot:

let listPublisher: ListPublisher = // ...
// ...
let objectPublisher = listPublisher.snapshot[indexPath]

(See ListPublisher examples below)

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 = 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.

(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 the EntityType:

class MyViewController: UIViewController, ObjectObserver {
    func objectMonitor(monitor: ObjectMonitor, willUpdateObject object: MyPersonEntity) {
        // ...
    }
    
    func objectMonitor(monitor: ObjectMonitor, didUpdateObject object: MyPersonEntity, changedPersistentKeys: Set) {
        // ...
    }
    
    func objectMonitor(monitor: ObjectMonitor, didDeleteObject object: MyPersonEntity) {
        // ...
    }
}

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 = 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.

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

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:

self.dataSource = DiffableDataSource.CollectionViewAdapter<Person>(
    collectionView: self.collectionView,
    dataStack: CoreStoreDefaults.dataStack,
    cellProvider: { (collectionView, indexPath, person) in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
        cell.setPerson(person)
        return cell
    }
)

// ...

listPublisher.addObserver(self) { [weak self] (listPublisher) in
   self?.dataSource?.apply(
       listPublisher.snapshot, animatingDifferences: true
   )
}

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 the EntityType:

class MyViewController: UIViewController, ListObserver {
    func listMonitorDidChange(monitor: ListMonitor) {
        // ...
    }
    
    func listMonitorDidRefetch(monitor: ListMonitor) {
        // ...
    }
}

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:
    func listMonitorWillChange(_ monitor: ListMonitor)
    func listMonitorDidChange(_ monitor: ListMonitor)
    func listMonitorWillRefetch(_ monitor: ListMonitor)
    func listMonitorDidRefetch(_ monitor: ListMonitor)

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:
    func listMonitor(_ monitor: ListMonitor, didInsertObject object: MyPersonEntity, toIndexPath indexPath: IndexPath)
    func listMonitor(_ monitor: ListMonitor, didDeleteObject object: MyPersonEntity, fromIndexPath indexPath: IndexPath)
    func listMonitor(_ monitor: ListMonitor, didUpdateObject object: MyPersonEntity, atIndexPath indexPath: IndexPath)
    func listMonitor(_ monitor: ListMonitor, didMoveObject object: MyPersonEntity, fromIndexPath: IndexPath, toIndexPath: IndexPath)
  • ListSectionObserver: in addition to ListObjectObserver methods, also lets you handle section inserts and deletes:
    func listMonitor(_ monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int)
    func listMonitor(_ monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int)

We then need to create a ListMonitor instance and register our ListObserver as an observer:

self.monitor = dataStack.monitorList(
    From<MyPersonEntity>()
        .where(\.age > 30)
        .orderBy(.ascending(\.name))
        .tweak { $0.fetchBatchSize = 20 }
)
self.monitor.addObserver(self)

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:

self.monitor = dataStack.monitorSectionedList(
    From<MyPersonEntity>()
        .sectionBy(\.age)
        .where(\.gender == "M")
        .orderBy(.ascending(\.age), .ascending(\.name))
        .tweak { $0.fetchBatchSize = 20 }
)

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:

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    let sectionInfo = self.monitor.sectionInfoAtIndex(section)
    return sectionInfo.name
}

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

Objective-C support

⚠️ Objective-C support is planned to be deprecated in a future CoreStore version.

All CoreStore types are still written in pure Swift, but most core types have Objective-C "bridging classes" that are visible to Objective-C code. To show a couple of usage examples:

Swift Objective-C
try dataStack.addStorageAndWait(SQLiteStore.self)
NSError *error
[CSCoreStore addSQLiteStorageAndWait:[CSSQLiteStore new] error:&error]
dataStack.perform(
    asynchronous: { (transaction) in
        // ...
    },
    completion: { (result) in
        switch result {
        case .success: print("Done")
        case .failure(let error): print(error)
        }
    }
)
[CSCoreStore beginAsynchronous:^(CSAsynchronousDataTransaction *transaction) {
    // ...
    [transaction 
     commitWithSuccess:^{
        NSLog(@"Done");
     }
     failure: ^(CSError *error) {
        NSLog(@"error: %@", result.error);
     }];
}];

All of these CS-prefixed bridging classes have very similar usage to the existing CoreStore APIs, and ironically none of them are written in Objective-C. This is very different to the common approach where apps and libraries write Objective-C APIs just to support both Objective-C and Swift. The advantage with CoreStore's approach is that your Swift codebase can already use the purely-Swift API without further changes in the future, but your "hybrid" codebase can still bridge instances back and forth from Objective-C to Swift.

For example, you may have a new, modern Swift class that holds a ListMonitor:

class MyViewController: UIViewController {
    let monitor = dataStack.monitorList(From<MyEntity>(), ...)
    // ...
}

Now let's say you have a legacy Objective-C class that previously uses NSFetchedResultsController. It's easy to switch from NSFetchedResultsController to CSListMonitor, but converting the rest of this huge class is impractical. You end up with

@interface MYOldViewController: UIViewController 
@property (nonatomic, readonly, strong) CSListMonitor* monitor;
- (instancetype)initWithMonitor:(CSListMonitor *)monitor;
@end

When you need to instantiate this class from Swift, you just call bridgeToObjectiveC:

class MyViewController: UIViewController {
    let monitor = dataStack.monitorList(From<MyEntity>(), ...)
    func showOldController() {
        let controller = MYOldViewController(monitor: self.monitor.bridgeToObjectiveC)
        self.presentViewController(controller, animated: true, completion: nil)
    }
}

Note that the CSListMonitor holds the exact same ListMonitor instance, which means that no copies and no extra fetching occur.

Objective-C syntax sugars

Objective-C tends to be verbose, so some method calls are long and unreadable. For example, fetching looks like this:

NSArray *objects = 
[CSCoreStore
 fetchAllFrom:[[CSFrom alloc] initWithEntityClass:[MYPerson class]]
 fetchClauses:@[[[CSWhere alloc] initWithFormat:@"%K == %@", @"isHidden", @NO],
                [[CSOrderBy alloc] initWithSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES],
                                                             [NSSortDescriptor sortDescriptorWithKey:@"firstName" ascending:YES]]]]];

Although it works, it looks terrible. For this, CoreStore provides CoreStoreBridge.h where these Objective-C calls are wrapped in readable, convenient macros and global functions. The call above becomes

NSArray *objects = 
[CSCoreStore
 fetchAllFrom:CSFromClass([MYPerson class])
 fetchClauses:@[CSWhereFormat(@"%K == %@", @"isHidden", @NO),
                CSOrderByKeys(CSSortAscending(@"lastName"),
                              CSSortAscending(@"firstName"), nil)]];

That's much shorter now. But we can still do better. Notice that we have strings being used as key paths. The CSKeyPath(...) macro gives us compile-time checking so keys that don't exist in a class will generate errors. Our key-safe code now looks like this:

NSArray *objects = 
[CSCoreStore
 fetchAllFrom:CSFromClass([MYPerson class])
 fetchClauses:@[CSWhereFormat(@"%K == %@", CSKeyPath(MYPerson, isHidden), @NO),
                CSOrderByKeys(CSSortAscending(CSKeyPath(MYPerson, lastName)),
                              CSSortAscending(CSKeyPath(MYPerson, firstName)), nil)]];

To use these syntax sugars, include CoreStoreBridge.h in your Objective-C source files.

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
}

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:

("Animal", isAbstract: true), Entity("Dog"), Entity("Person") ] ) ) CoreStoreDefaults.dataStack.addStorage(/* ... */) ">
CoreStoreDefaults.dataStack = DataStack(
    CoreStoreSchema(
        modelVersion: "V1",
        entities: [
            Entity<Animal>("Animal", isAbstract: true),
            Entity<Dog>("Dog"),
            Entity<Person>("Person")
        ]
    )
)
CoreStoreDefaults.dataStack.addStorage(/* ... */)

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 strings

let keyPath = String(keyPath: \Dog.$nickname)

as well as Where and OrderBy clauses

let puppies = try dataStack.fetchAll(
    From<Dog>()
        .where(\.$age < 5)
        .orderBy(.ascending(\.$age))
)

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
("nickname") } ">
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
("species", initial: "") static func getSpeciesPlural(_ partialObject: PartialObject) -> String? { let species = partialObject.value(for: { $0.species }) return species + "s" } } ">
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) -> 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:

class Person: CoreStoreObject {
    
    struct CustomInfo: Codable {
        // ...
    }
    
    @Field.Coded("otherInfo", coder: FieldCoders.Json.self)
    var otherInfo: CustomInfo?
    
    @Field.Coded(
        "photo",
        coder: {
            encode: { $0.toData() },
            decode: { Photo(fromData: $0) }
        }
    )
    var photo: Photo?
}

‼️ 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 : To-one relationship
  • Array : To-many ordered relationship
  • Set : To-many unordered relationship
Before
@Field.Stored
("pets", inverse: \.$master) } ">
class Pet: CoreStoreObject {
    
    let master = Relationship.ToOne<Person>("master")
}
class Person: CoreStoreObject {
    
    let pets: Relationship.ToManyUnordered("pets", inverse: \.$master)
}
} ">
class Pet: CoreStoreObject {
    
    @Field.Relationship("master")
    var master: Person?
}
class Person: CoreStoreObject {
    
    @Field.Relationship("pets", inverse: \.$master)
    var pets: Set
}

⚠️ 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.where(\.title == "Mr.")
  • After: From.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:

VersionLock

Copy this dictionary value and use it as the versionLock: argument of the CoreStoreSchema initializer:

("Animal", isAbstract: true), Entity("Dog"), Entity("Person"), ], versionLock: [ "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a], "Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7], "Person": [0x66d8bbfd8b21561f, 0xcecec69ecae3570f, 0xc4b73d71256214ef, 0x89b99bfe3e013e8b] ] ) ">
CoreStoreSchema(
    modelVersion: "V1",
    entities: [
        Entity<Animal>("Animal", isAbstract: true),
        Entity<Dog>("Dog"),
        Entity<Person>("Person"),
    ],
    versionLock: [
        "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
        "Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7],
        "Person": [0x66d8bbfd8b21561f, 0xcecec69ecae3570f, 0xc4b73d71256214ef, 0x89b99bfe3e013e8b]
    ]
)

You can also get this hash after the DataStack has been fully set up by printing to the console:

print(CoreStoreDefaults.dataStack.modelSchema.printCoreStoreSchema())

Once the version lock is set, any changes in the properties or to the model will trigger an assertion failure similar to this:

VersionLock failure

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 MigrationProgress 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.

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:

dataStack.reactive
    .importUniqueObjects(
        Into<Person>(),
        sourceArray: [
            ["name": "John"],
            ["name": "Bob"],
            ["name": "Joe"]
        ]
    )
    .sink(
        receiveCompletion: { result in
            // ...
        },
        receiveValue: { (people) in
            XCTAssertEqual(people?.count, 3)
            // ...
        }
    )
    .store(in: &cancellables)

ListPublisher.reactive

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

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

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

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

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

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

init(listPublisher: ListPublisher) {
   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

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?

init(objectPublisher: ObjectPublisher) {
   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]

var body: some View {
    
    List {
        
        ForEach(self.array) { objectSnapshot in
            
            // ...
        }
    }
}
Signature:
ForEach(objectIn: ListSnapshot<O>)
Closure:
ObjectPublisher<O>
let listSnapshot: ListSnapshot

var body: some View {
    
    List {
        
        ForEach(objectIn: self.listSnapshot) { objectPublisher in
            
            // ...
        }
    }
}
Signature:
ForEach(objectIn: [ObjectSnapshot<O>])
Closure:
ObjectPublisher<O>
let array: [ObjectSnapshot]

var body: some View {
    
    List {
        
        ForEach(objectIn: self.array) { objectPublisher in
            
            // ...
        }
    }
}
Signature:
ForEach(sectionIn: ListSnapshot<O>)
Closure:
[ListSnapshot<O>.SectionInfo]
let listSnapshot: ListSnapshot

var body: some View {
    
    List {
        
        ForEach(sectionIn: self.listSnapshot) { sectionInfo in
            
            // ...
        }
    }
}
Signature:
ForEach(objectIn: ListSnapshot<O>.SectionInfo)
Closure:
ObjectPublisher<O>
let listSnapshot: ListSnapshot

var body: some View {
    
    List {
        
        ForEach(sectionIn: self.listSnapshot) { sectionInfo in
            
            ForEach(objectIn: sectionInfo) { objectPublisher in
               
                // ...
            }
        }
    }
}

Roadmap

Prototyping stage

  • Widget/Extensions storage-sharing support
  • CloudKit support

Under consideration

  • Derived attributes
  • Cross-storage relationships (via Fetched Attributes)

Installation

  • Requires:
  • Dependencies:
    • None
  • Other notes:
    • 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', '~> 8.0'

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

= 8.0.0 ">
github "JohnEstropia/CoreStore" >= 8.0.0

and run

carthage update

This installs CoreStore as a framework. Declare import CoreStore in your swift file to use the library.

Install with Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/JohnEstropia/CoreStore.git", from: "8.0.1"))
]

Declare import CoreStore in your swift file to use the library.

Install as Git Submodule

git submodule add https://github.com/JohnEstropia/CoreStore.git 

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

CoreStore

where JohnEstropia is the Owner (forks may appear as well). Then add to your project.

Objective-C support

To use the Objective-C syntax sugars, import CoreStoreBridge.h in your .m source files.

Changesets

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

Comments
  • Accepting feedback on the new `CoreStoreObject` system

    Accepting feedback on the new `CoreStoreObject` system

    If anyone is already using the new CoreStoreObject, I would gladly hear your your feedback, observations, and suggestions 🙂

    Right now the CoreStoreObject more or less is as functional as NSManagedObject in common scenarios, but I might be missing some use-cases so I would like to hear everyone's opinion.

    help wanted 
    opened by JohnEstropia 51
  • iOS 10: Sectioned ListMonitor crashing with EXC_BAD_ACCESS

    iOS 10: Sectioned ListMonitor crashing with EXC_BAD_ACCESS

    Since upgrading to CoreStore v2.1.0 and Xcode 8 I'm getting a repeatable crash whenever I try to display a view that is backed by a sectioned ListMonitor on the iOS 10 simulator (it's fine on iOS 9):

    screen shot 2016-09-22 at 17 43 32 screen shot 2016-09-22 at 17 44 21

    If I change the monitor to a non-sectioned list it's fine.

    I've tried enabling Zombie Objects but nothing helpful is logged.

    Also, why is CoreStore warning me that I'm overwriting a Where clause? Isn't that the point of refetch() anyway?

    I'm at a bit of a loss at how to fix this one, any help would be much appreciated.

    fixed ios/compiler bug 
    opened by jamesbebbington 44
  • Build fails on Xcode 8 / macOS Sierra (both for Swift 2.3 and 3)

    Build fails on Xcode 8 / macOS Sierra (both for Swift 2.3 and 3)

    When trying to build CoreStore via Carthage (for use in one of my iOS apps) I'm running into this compiler crash:

    Stored value type does not match pointer operand type!
      store %C9CoreStore21CSBaseDataTransaction* %23, %C9CoreStore28CSSynchronousDataTransaction** %0, align 8, !dbg !2529
     %C9CoreStore28CSSynchronousDataTransaction*LLVM ERROR: Broken function found, compilation aborted!
    2016-10-20 13:46:56.454 xcodebuild[69083:526859]  DVTAssertions: Warning I in /Library/Caches/com.apple.xbs/Sources/IDEFrameworks/IDEFrameworks-11246/IDEFoundation/Playgrounds/IDEPlaygroundAuxiliarySourceCompilerOperation.m:386
    Details:  Unable to read diagnostics from file "/Users/shirk/Library/Developer/Xcode/DerivedData/CoreStore-dibsnzstxqdaakfnrpbesflgtiep/Build/Intermediates/CoreStore.build/Release/CoreStore OSX.build/Objects-normal/x86_64/CSListMonitor.dia" (Invalid File): Invalid diagnostics signature
    Function: void XCGenerateDiagnosticsFromFile(NSString *__strong, NSString *__strong, NSDictionary *__strong, NSDictionary *__strong, IDEActivityLogSectionRecorder *__strong, BOOL (^__strong)(IDEActivityLogMessage *__strong))
    Thread:   <NSThread: 0x7fbbc7b16f40>{number = 15, name = (null)}
    Please file a bug at http://bugreport.apple.com with this warning message and any useful information you can provide.
    
    Command /Applications/Xcode.app/Contents/Developer/Toolchains/Swift_2.3.xctoolchain/usr/bin/swiftc failed with exit code 1
    ** BUILD FAILED **
    
    
    The following build commands failed:
        CompileSwift normal x86_64
        CompileSwiftSources normal x86_64 com.apple.xcode.tools.swift.compiler
    

    Looking around the net this seems to be an issue with the swift compiler itself, but one that can be worked around be the code that triggers it.

    I know it's not the fault of CoreStore but as it is I can't build the framework in any version (master / 2.1.0 / swift_3) and there is no saying when a fixed swift compiler will be included in a new Xcode release.

    fixed ios/compiler bug 
    opened by Shirk 32
  • ListMonitor throws EXC_BAD_ACCESS on deinit

    ListMonitor throws EXC_BAD_ACCESS on deinit

    After having upgraded from CoreStore v1.4.4 to v1.6.1 I'm getting a repeatable crash when my ListMonitor is deinitialized when the controller it is a property of is deallocated:

    screen shot 2016-03-29 at 10 40 30

    My ListMonitor is instantiated like so:

    class ProjectController: UITableViewController, ListSectionObserver, TableViewDisabling {
    
        …
    
        var project: Project? {
            didSet {
                guard let project = self.project else { return }
                questions.refetch(Where("project.id == %d", project.id))
            }
        }
    
        /// Questions monitor
        let questions = CoreStore.monitorSectionedList(
            From(Question),
            SectionBy("category.name"),
            OrderBy(.Ascending("position")),
            Where(false) // Overridden in .project property observer
        )
    
        …
    
    }
    

    Explicitly removing the observer when the controller is deallocated doesn't help.

    I'm not sure how to go about debugging this one, could you give me some pointers please.

    Thanks.

    corestore bug fixed 
    opened by jamesbebbington 24
  • carthage issue

    carthage issue

    When adding the project as a carthage dependency i get the following error:

    A shell task failed with exit code 128: Permission denied (publickey). fatal: Could not read from remote repository.

    Please make sure you have the correct access rights and the repository exists.

    fixed 
    opened by demonar 21
  • context.parentStack == nil

    context.parentStack == nil

    Hi. I found that in some cases in row fetchRequest.entity = context.parentStack!.entityDescription(for: EntityIdentifier(self.entityClass))! context.parentStack can be nil. But i dont understand why? Can you help me?

    This From.swift : 144

    opened by dufflink 20
  • Memory warning

    Memory warning

    I have a problem when using CoreStore. After I do an update (big transaction) on the db some objects are not destroyed and memory increases until a memory warning is received and the app is closed. What should I do for a better memory management in a transaction?

    corestore bug fixed 
    opened by nikolovdragan 18
  • Query Attributes from Subentities

    Query Attributes from Subentities

    I'd like to query attributes from my entity and one of it's sub-entities. I've tried few tweaks, but with no luck. This is what I have so far:

            // Rank List
            guard let aggregate = CoreStore.queryAttributes(
                From(CDCrystal),
                Select("employeeId",
                    "employee.firstName",
                    "employee.lastName",
                    .Sum("amount", As: "crystals")),
                Tweak { (fetchRequest) -> Void in
                    fetchRequest.returnsObjectsAsFaults = false
                    fetchRequest.includesSubentities = true
                }
                )
            else {
                return 0
            }
    

    However in console I'm getting:

    Crystals Relation Error: Employee with ID: Optional(3995) not found .... (Same for other employees)

    and in the result array I've got dictionaries with only two properties (employeeId and crystals).


    Just to complete my idea - next I want to sort results like this:

            let ordered = aggregate.sort {
                if (($0["crystals"] as! NSNumber).integerValue == ($1["crystals"] as! NSNumber).integerValue) {
                    // TODO: Sort By name
                } else {
                    return ($0["crystals"] as! NSNumber).integerValue < ($1["crystals"] as! NSNumber).integerValue
                }
            }
    
    

    Maybe I'm missing something. I'll appreciate any suggestions.

    corestore bug fixed 
    opened by ghost 18
  • Get transaction that wraps a managed context

    Get transaction that wraps a managed context

    I frequently bump into situation when I'd like to do smth in the context of a specific object. In pure CoreData, I'd run those operation on object.managedObjectContext and all is good. Now've to keep track of both the object and transaction it's in. This adds quite a a bit of unnecessary complexity to the code.

    I see there's NSManagedObjectContext#parentTransaction method that does exactly what I was looking for. But it's marked as internal. Is there a reason why I shouldn't touch it? Is there some common pattern how to work around this issue?

    enhancement question fixed 
    opened by mantas 18
  • Main Thread Deadlock

    Main Thread Deadlock

    It looks like it's possible to get the main thread stuck in a deadlock - seen on iOS 10 GM on an iPhone 5S.

    There's nothing crazy about this case. There are two transactions, one synchronous, one "implicit" (reading directly from the main context through dataStack.fetchOne(). The resulting stacks at the time of the deadlock are at the bottom of this.

    As best I can tell:

    1. Main thread executes a performBlockAndWait, and begins executing its closure
    2. Background thread saves a synchronous transaction
    3. Background thread observer monitors the change on the background context
    4. Background thread executes performBlockAndWait to merge changes into the main context, and parks waiting for a lock on the context
    5. Main thread attempt to acquire a lock in executeFetchRequest, and parks waiting for the same lock that the background thread is trying to get in step 4

    I would think that when using a context with a main queue concurrency type on the main thread, there would be optimizations avoiding the need to acquire locks. The fact that you're executing in the main thread is enough to ensure nobody else can be executing on the main thread.

    However, it appears that, while performBlockAndWait passes through just fine, executeFetchRequest attempts to lock (the context? the thread?). However, at this point the background is already first in line for that lock.

    I can work around this issue by using asynchronous transactions on the background thread, but this raises a number of questions:

    1. Is there a reason merging changes should be done synchronously? Even in a SynchronousDataTransaction, you're really asking that the write to the root saving context is performed synchronously. This can be done without merging the changes to the main queue synchronously. Given that CoreStore ensures transactions run synchronously, and GCD ensures fairness in the execution of dispatch blocks, as long as each transaction executes performBlock (async) to merge their changes into the main queue, they should all merge in the order they were committed, which means the only reason for failure should be caused memory pressure, as I understand it. Is this assumption wrong? If it can't really get out of sync, why force the merge to be synchronous?
    2. How am I not seeing this more often? If I'm right about the cause, it seems like this deadlock should happen any time that a background synchronous context attempts to save before, but on the same runloop cycle, as the main context attempts to execute a block. In practice, I've seen this one time after launching my app hundreds of times. Is there possibly something more nuanced here that I'm missing?
    3. Is SynchronousDataTransaction safe at all? Having UnsafeDataTransaction implies that either the sync/async variants are safe counterparts, but given such a simple example as this - it seems that the only truly safe transaction is the AsynchronousDataTransaction.

    Stack

    Main Thread:

    #0  0x1b36c55c in __ulock_wait ()
    #1  0x02aa8b14 in _dispatch_ulock_wait ()
    #2  0x02aa8c12 in _dispatch_thread_event_wait_slow ()
    #3  0x02a92854 in _dispatch_barrier_sync_f_slow ()
    #4  0x1dbb4b7c in _perform ()
    #5  0x1dbc584e in -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:] ()
    #6  0x1db1d590 in -[NSManagedObjectContext executeFetchRequest:error:] ()
    #7  0x01014a94 in NSManagedObjectContext.(fetchOne<A where ...> (NSFetchRequest) -> A?).(closure #1) at /Project/Pods/CoreStore/Sources/Internal/NSManagedObjectContext+Querying.swift:108
    #8  0x01014d28 in partial apply for NSManagedObjectContext.(fetchOne<A where ...> (NSFetchRequest) -> A?).(closure #1) ()
    #9  0x00fa4a88 in thunk ()
    #10 0x1dbba27e in developerSubmittedBlockToNSManagedObjectContextPerform ()
    #11 0x1dbba11e in -[NSManagedObjectContext performBlockAndWait:] ()
    #12 0x01014460 in NSManagedObjectContext.fetchOne<A where ...> (NSFetchRequest) -> A? at /Project/Pods/CoreStore/Sources/Internal/NSManagedObjectContext+Querying.swift:114
    #13 0x01014038 in NSManagedObjectContext.fetchOne<A where ...> (From<A>, [FetchClause])
    

    Background Thread:

    Thread 4Queue : NSManagedObjectContext 0x14ec1950: com.corestore.rootcontext (serial)
    #0  0x1b36c55c in __ulock_wait ()
    #1  0x02aa8b14 in _dispatch_ulock_wait ()
    #2  0x02aa8c12 in _dispatch_thread_event_wait_slow ()
    #3  0x02a92854 in _dispatch_barrier_sync_f_slow ()
    #4  0x1dbba144 in -[NSManagedObjectContext performBlockAndWait:] ()
    #5  0x0101edfc in static NSManagedObjectContext.(mainContextForRootContext(NSManagedObjectContext) -> NSManagedObjectContext).(closure #1) at /Project/Pods/CoreStore/Sources/Internal/NSManagedObjectContext+Setup.swift:126
    #6  0x0100a638 in thunk ()
    #7  0x1c3f6fb0 in -[__NSObserver _doit:] ()
    #8  0x1bb6d760 in __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ ()
    #9  0x1bb6d09c in _CFXRegistrationPost ()
    #10 0x1bb6ce80 in ___CFXNotificationPost_block_invoke ()
    #11 0x1bbc881c in -[_CFXNotificationRegistrar find:object:observer:enumerator:] ()
    #12 0x1bacd09c in _CFXNotificationPost ()
    #13 0x1c3f4782 in -[NSNotificationCenter postNotificationName:object:userInfo:] ()
    #14 0x1dbc01a4 in -[NSManagedObjectContext(_NSInternalNotificationHandling) _postContextDidSaveNotificationWithUserInfo:] ()
    #15 0x1db517d6 in -[NSManagedObjectContext(_NSInternalAdditions) _didSaveChanges] ()
    #16 0x1db3df98 in -[NSManagedObjectContext save:] ()
    #17 0x01020c00 in NSManagedObjectContext.(saveSynchronously() -> SaveResult).(closure #1) at /Project/Pods/CoreStore/Sources/Internal/NSManagedObjectContext+Transaction.swift:118
    #18 0x00fa4a88 in thunk ()
    #19 0x1dbba27e in developerSubmittedBlockToNSManagedObjectContextPerform ()
    #20 0x02a83d52 in _dispatch_client_callout ()
    #21 0x02a8f2e2 in _dispatch_barrier_sync_f_invoke ()
    #22 0x1dbba144 in -[NSManagedObjectContext performBlockAndWait:] ()
    #23 0x010207bc in NSManagedObjectContext.saveSynchronously() -> SaveResult at /Project/Pods/CoreStore/Sources/Internal/NSManagedObjectContext+Transaction.swift:147
    #24 0x01020d08 in NSManagedObjectContext.(saveSynchronously() -> SaveResult).(closure #1) at /Project/Pods/CoreStore/Sources/Internal/NSManagedObjectContext+Transaction.swift:134
    #25 0x00fa4a88 in thunk ()
    #26 0x1dbba27e in developerSubmittedBlockToNSManagedObjectContextPerform ()
    #27 0x02a8f2e2 in _dispatch_barrier_sync_f_invoke ()
    #28 0x1dbba144 in -[NSManagedObjectContext performBlockAndWait:] ()
    #29 0x010207bc in NSManagedObjectContext.saveSynchronously() -> SaveResult at /Project/Pods/CoreStore/Sources/Internal/NSManagedObjectContext+Transaction.swift:147
    #30 0x010531b4 in SynchronousDataTransaction.commitAndWait() -> SaveResult at
    
    enhancement fixed ios/compiler bug 
    opened by colinmorelli 16
  • Noob Question : Access to default context and NSFetchedResultsController

    Noob Question : Access to default context and NSFetchedResultsController

    John,

    Just getting started with CoreStore, so...

    If I 'needed' to use another complementary framework that needs a NSFetchedResultsController and a context for reading//writing, what is the safest way to create/access them without breaking a CoreStore dataStack set up like so:

    struct DataStoreService {
    
        static let coreDataStack: DataStack = {
    
            let dataStack = DataStack(modelName: "AppName")
    
            do {
                try dataStack.addSQLiteStoreAndWait(
                    fileName: "AppName.sqlite",
                    configuration:nil,
                    resetStoreOnModelMismatch: true
                )
    
            } catch {
                print("Failed creating dataStack with error: \(error as NSError)")
            }
            return dataStack
        }()
    
    }
    

    And the stack is used from an interactor like structure using something like this:

      func fetchAllWidgets() -> [WidgetsMO]
      {
        return DataStoreService.coreDataStack.fetchAll(From(WidgetsMO))!
       }
    

    I 'think' what I'm looking for is just a way to access the correct context to used for the NSFetchedResultsController (yours ideally, so I can use CoreStore fetching and queries) and the best way to deal with CoreStore transactions. If I can't access your NSFetchedController I would create one. Monitors and Observers, with my level of knowledge about CoreStore, seem a little beyond me.

    And what are the pitfalls and downside to doing something like this?

    The library is https://github.com/DenHeadless/DTTableViewManager by the way (which lightens up tableViews by using a manager (https://github.com/DenHeadless/DTModelStorage) to bind the model against tableViewCells), and I envision using it primarily for reads and using the CoreStore functionality for hopefully all of the writes.

    Basically, I would really like to somehow wire CoreStore and DTTableViewManager together.

    I want to make sure I'm aware of all of the potential issues and pick the best approach before I go any further.

    Thanx in Advance

    question 
    opened by wm-j-ray 16
  • Automatic Lightweight Migration without explicitly create model versions?

    Automatic Lightweight Migration without explicitly create model versions?

    HI @JohnEstropia i would like to ask about lightweight migration in CoreStore i watch at WWDC video https://developer.apple.com/videos/play/wwdc2022/10120/ that we can do lightweight migration without creating new model version.

    Does CoreStore has support to do that? Since i have many model/table that will be a tedious process to duplicate previous model into new version and make adjustment in new version model

    thank you in advance 🙏

    question 
    opened by dwirandytlvk 3
  • Add or update child Table in 1 to 1 relationship

    Add or update child Table in 1 to 1 relationship

    Hallo @JohnEstropia I'am trying this library that seems great, i want to ask about how add / update unique object

    for example i have 2 object FlightBookingInfo has one FlightBookingDetail, i want to update FlightBookingInfo and also FlightBookingDetail from my json that i got from the API, i can add or update FlightBookingInfo but FlightBookingDetail value is inserted instead of update existing value that has relationship to FlightBookingInfo

    May i know how to update FlightBookingDetail from json, without fetching it first from database?

    FlightBookingInfo

    class FlightBookingInfo: CoreStoreObject, ImportableUniqueObject {
        
        typealias UniqueIDType = String
        
        static var uniqueIDKeyPath: String = String(keyPath: \FlightBookingInfo.$invoiceId)
        
        typealias ImportSource = [String: Any]
        
        @Field.Stored("invoiceId")
        var invoiceId: String = ""
        
        @Field.Stored("bookingStatus")
        var bookingStatus: String = ""
        
        @Field.Stored("rescheduleId")
        var rescheduleId: String = ""
        
        @Field.Relationship("flightBookingDetail", inverse: \.$flightBookingInfo, deleteRule: .cascade)
        var flightBookingDetail: FlightBookingDetail?
        
        static func uniqueID(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws -> String? {
            return source["invoiceId"] as? String
        }
        
        func update(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws {
            self.invoiceId = source["invoiceId"] as? String ?? ""
            self.bookingStatus = source["bookingStatus"] as? String ?? ""
            self.rescheduleId = source["rescheduleId"] as? String ?? ""
            self.flightBookingDetail = try transaction.importObject(Into<FlightBookingDetail>(), source: source)
        }
    }
    

    FlightBookingDetail

    class FlightBookingDetail: CoreStoreObject, ImportableUniqueObject {
        
        static func uniqueID(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws -> String? {
            return source["invoiceId"] as? String
        }
        
        typealias UniqueIDType = String
        
        static var uniqueIDKeyPath: String = String(keyPath: \FlightBookingInfo.$invoiceId)
        
        func update(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws {
            let flightBookingDetailSource = source["flightBookingDetail"] as? [String: Any] ?? [:]
            multipleAirlines = flightBookingDetailSource["multipleAirlines"] as? Bool ?? false
            twoWay = flightBookingDetailSource["twoWay"] as? Bool ?? false
            originDate = flightBookingDetailSource["originDate"] as? Date
            returnFlightDate = flightBookingDetailSource["returnFlightDate"] as? Date
        }
        
        
        typealias ImportSource = [String: Any]
        
        @Field.Stored("multipleAirlines")
        var multipleAirlines: Bool = false
        
        @Field.Stored("twoWay")
        var twoWay: Bool = false
        
        @Field.Stored("originDate")
        var originDate: Date?
        
        @Field.Stored("returnFlightDate")
        var returnFlightDate: Date?
        
        @Field.Relationship("flightBookingInfo")
        var flightBookingInfo: FlightBookingInfo?
    }
    

    Here is how i add/update the data

          let jsonData: [String: Any] = [
                "invoiceId": "1111",
                "bookingStatus": "\(["failed", "success", "pending"].randomElement()!)",
                "rescheduleId": "\(Int.random(in: 1000..<2000))",
                "flightBookingDetail": [
                    "multipleAirlines": Bool.random(),
                    "twoWay": Bool.random(),
                    "originDate": Date.now,
                    "returnFlightDate":  Calendar.current.date(byAdding: .day, value: 1, to: Date.now),
                ]
            ]
            
            CoreDataManager.instance.dataStack?.perform(
                asynchronous: { transaction in
                    try transaction.importUniqueObject(Into<FlightBookingInfo>(), source: jsonData)
                }, success: { _ in
                    print("add / update FlightBookingInfo success")
                }, failure: { _ in
                    print("add / update FlightBookingInfo failed")
                }
            )
    
    opened by dwirandytlvk 0
  • How to perform multi table associated query?

    How to perform multi table associated query?

    Hello, and thanks for the great library

    I want to perform an associated query on multiple tables based on the same field (such as ID) in multiple tables. How do I query?

    I hope I can get your help!

    Thank you.

    opened by chenchenSJ 0
  • Realtime saving

    Realtime saving

    Hi, I am starting to integrate your excellent project into my app. I am now facing a problem.

    How do I save to the database synchronously? I need realtime saving.

    I need to replace this function

    do { try MY_OBJECT.managedObjectContext?.save() } catch let error { fatalError("Failure to save context: (error)") }

    I thank you in advance for your response. Thank you

    opened by paoloandrea 2
  • How to create a func() to build OrderBy clauses

    How to create a func() to build OrderBy clauses

    Hello, and thanks for the great library

    I would like to create a function to avoid duplicate code in sort selection (having a table view with 4 different sort asc/desc like func buildSort(section: Section, sort: Sort, sortAscending: Bool) -> OrderBy

    But I have issues on how to define in function declaration the -> OrderBy return parameter

    Any help?

    Thanks

    question 
    opened by appfabtech 1
  • fetchAll returning empty array

    fetchAll returning empty array

    I have an app which stores an entity class EntityA: NSManagedObject {...} I have the following simplified CoreData code to fetch this entity

        let container = NSPersistentContainer(name: "Model")
        let context = container.viewContext
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: container.managedObjectModel)
        
        let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let persistentStoreURL = documentsPath.appendingPathComponent("Model.sqlite")
        
        try! coordinator.addPersistentStore(
          ofType: NSSQLiteStoreType,
          configurationName: nil,
          at: persistentStoreURL,
          options: [:]
        )
    
        container.loadPersistentStores { _, _ in }
        
        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "EntityA")
        try! context.fetch(fetchRequest) as! [EntityA] // array populated with multiple objects
    

    I created the equivalent CoreStore code

        let dataStack = DataStack(xcodeModelName: "Model")
    
        let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let sqliteFileUrl = documentsPath.appendingPathComponent("Model.sqlite")
        
        let storage = SQLiteStore(fileURL: sqliteFileUrl)
        try! dataStack.addStorageAndWait(storage)
    
        try! dataStack.fetchAll(From<ModelA>(nil)) // array empty
    

    The CoreData code returns the properly populated array while CoreStore returns an empty array. I tried initializing SQLiteStore with a number of different options, and tried the async versions of addStorage all to no avail. I set breakpoints in the relevant CoreStore functions, but could not find anything glaring that would indicate the issue. Not sure if this is a bug or if I am misusing CoreStore but any help here would be much appreciated.

    ios/compiler bug 
    opened by darrenasaro 10
Releases(9.0.0)
  • 9.0.0(Sep 29, 2022)

    Swift 2.7 is bundled with Xcode 14, and CoreStore 9.0.0 will be the officially supported version from here on out.

    Breaking changes:

    • Removal of Objective-C support (which had been deprecated for a long time now)
    • Migration to Swift 2.7 from Swift 2.4
    • Bumped minimum supported version to iOS 13 from iOS 11. While this double jump had never been done in CoreStore before, we are aiming to fully utilize Combine utilities internally and to make the shift to Swift concurrency (which requires at least iOS 13) as smooth as possible.

    Full Changelog: https://github.com/JohnEstropia/CoreStore/compare/8.1.0...9.0.0

    Source code(tar.gz)
    Source code(zip)
  • 8.0.0(Apr 11, 2021)

    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 MigrationProgress enum value. The .migrating value is only emitted if the storage goes through a migration.

    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:

    dataStack.reactive
        .importUniqueObjects(
            Into<Person>(),
            sourceArray: [
                ["name": "John"],
                ["name": "Bob"],
                ["name": "Joe"]
            ]
        )
        .sink(
            receiveCompletion: { result in
                // ...
            },
            receiveValue: { (people) in
                XCTAssertEqual(people?.count, 3)
                // ...
            }
        )
        .store(in: &cancellables)
    

    ListPublisher.reactive

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

    DataExample
    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
    // ... } } } }
    Source code(tar.gz)
    Source code(zip)
  • 7.3.0(Sep 19, 2020)

    New Demo app

    The old CoreStoreDemo app has been renamed to LegacyDemo, and a new Demo app now showcases CoreStore features through SwiftUI:

    Don't worry, standard UIKit samples are also available (thanks to UIViewControllerRepresentable)

    Feel free to suggest improvements to the Demo app!

    Swift 5.3 / Xcode 12 / iOS 14 Support

    CoreStore now compiles using Xcode 12 and Swift 5.3!

    ⚠️ There was a bug in Swift 5.3 propertyWrappers where Segmentation Faults happen during compile time. CoreStore was able to work around this issue through runtime fatalErrors, but the result is that missing required parameters for @Field properties may not be caught during compile-time. The runtime checks crash if there are missing parameters, so please take care to debug your models!

    Source code(tar.gz)
    Source code(zip)
  • 7.2.0(Jun 20, 2020)

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

    Source code(tar.gz)
    Source code(zip)
  • 7.1.0(Mar 27, 2020)

    Maintenance updates

    • Xcode 11.4 and Swift 5.2 support

    New Property Wrappers syntax

    ⚠️ These changes apply only to CoreStoreObject subclasses, notNSManagedObjects.

    ‼️ Please take note of the warnings below before migrating 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.

    ‼️ If you are confident about conversion, I cannot stress this enough, but please make sure to set your schema's VersionLock before converting!

    @Field.Stored (replacement for non "transient" Value.Required and Value.Optional)

    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. ⚠️ 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 (replacement for "transient" versions of Value.Required andValue.Optional)

    class Animal: CoreStoreObject {
    
        @Field.Virtual(
            "pluralName",
            customGetter: { (object, field) in
                return object.$species.value + "s"
            }
        )
        var pluralName: String
    
        @Field.Stored("species")
        var species: String = ""
    }
    

    ⚠️ Only Value.Required and Value.Optional that ARE transient values can be converted to Field.Virtual. ⚠️ 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 (replacement for Transformable.Required andTransformable.Optional, with additional support for custom encoders such as JSON)

    class Person: CoreStoreObject {
    
        @Field.Coded(
            "bloodType",
            coder: {
                encode: { $0.toData() },
                decode: { BloodType(fromData: $0) }
            }
        )
        var bloodType: BloodType?
    }
    

    ‼️ The current Transformable.Required and Transformable.Optional mechanism have no safe conversion to @Field.Coded. Please use @Field.Coded only for newly added attributes.

    @Field.Relationship (replacement for Relationship.ToOne, Relationship.ToManyOrdered, and Relationship.ToManyUnordered)

    class Pet: CoreStoreObject {
    
        @Field.Relationship("master")
        var master: Person?
    }
    class Person: CoreStoreObject {
    
        @Field.Relationship("pets", inverse: \.$master)
        var pets: Set<Pet>
    }
    

    ⚠️ Relationship.ToOne<T> maps to T?, Relationship.ToManyOrdered maps to Array<T>, and Relationship.ToManyUnordered maps to Set<T> ⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model's hash might change.

    Usage

    Before diving into the properties themselves, note that they will effectively force you to use a different syntax for queries:

    • Before: From<Person>.where(\.title == "Mr.")
    • After: From<Person>.where(\.$title == "Mr.")

    There are a several advantages to using these Property Wrappers:

    • The @propertyWrapper versions will be magnitudes performant and efficient than their current implementations. Currently Mirror reflection is used a lot to inject the NSManagedObject reference into the properties. With @propertyWrappers this will be synthesized by the compiler for us. (See https://github.com/apple/swift/pull/25884)
    • The @propertyWrapper versions, being structs, will give the compiler a lot more room for optimizations which were not possible before due to the need for mutable classes.
    • You can now add computed properties that are accessible to both ObjectSnapshots and ObjectPublishers by declaring them as @Field.Virtual. Note that for ObjectSnapshots, the computed values are evaluated only once during creation and are not recomputed afterwards.

    The only disadvantage will be:

    • You need to update your code by hand to migrate to the new @propertyWrappers (But the legacy ones will remain available for quite a while, so while it is recommended to migrate soon, no need to panic)
    Source code(tar.gz)
    Source code(zip)
  • 7.0.0(Oct 22, 2019)

    ⚠️This update will break current code. Make sure to read the changes below:

    Breaking Changes

    Starting version 7.0.0, CoreStore will be using a lot of Swift 5.1 features, both internally and in its public API. You can keep using the last 6.3.2 release if you still need Swift 5.0.

    Deprecations

    The CoreStore-namespaced API has been deprecated in favor of DataStack method calls. If you are using the global utilities such as CoreStore.defaultStack and CoreStore.logger, a new CoreStoreDefaults namespace has been provided:

    • CoreStore.defaultStack -> CoreStoreDefaults.dataStack
    • CoreStore.logger -> CoreStoreDefaults.logger
    • CoreStore.addStorage(...) -> CoreStoreDefaults.dataStack.addStorage(...)
    • CoreStore.fetchAll(...) -> CoreStoreDefaults.dataStack.fetchAll(...)
    • etc.

    If you have been using your own properties to store DataStack references, then you should not be affected by this change.

    New features

    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!

    DiffableDataSource.CollectionView (iOS and macOS) and DiffableDataSource.TableView (iOS)

    self.dataSource = DiffableDataSource.CollectionView<Person>(
        collectionView: self.collectionView,
        dataStack: CoreStoreDefaults.dataStack,
        cellProvider: { (collectionView, indexPath, person) in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
            cell.setPerson(person)
            return cell
        }
    )
    

    This is now the recommended method of reloading UITableViews and UICollectionViews because it uses list diffing to update your list views. This means that it is a lot less prone to cause layout errors.

    ListPublisher and ListSnapshot

    ListPublisher is a more lightweight counterpart of ListMonitor. Unlike ListMonitor, it does not keep track of minute inserts, deletes, moves, and updates. It simply updates its snapshot property which is a struct storing the list state at a specific point in time. This ListSnapshot is then usable with the DiffableDataSource utilities (See section above).

    self.listPublisher = dataStack.listPublisher(
        From<Person>()
            .sectionBy(\.age") { "Age \($0)" } // sections are optional
            .where(\.title == "Engineer")
            .orderBy(.ascending(\.lastName))
    )
    self.listPublisher.addObserver(self) { [weak self] (listPublisher) in
        self?.dataSource?.apply(
           listPublisher.snapshot, animatingDifferences: true
       )
    }
    

    ListSnapshots store only NSManagedObjectIDs and their sections.

    ObjectPublisher and ObjectSnapshot

    ObjectPublisher is a more lightweight counterpart of ObjectMonitor. Unlike ObjectMonitor, it does not keep track of per-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 objectPublisher = self.listPublisher.snapshot[indexPath]
    

    The ObjectPublisher exposes a snapshot property which returns an ObjectSnapshot, which is a lazily generated struct containing fully-copied property values.

    objectPublisher.addObserver(self) { [weak self] (objectPublisher) in
        let snapshot: ObjectSnapshot<Person> = objectPublisher.snapshot
        // handle changes
    }
    

    This snapshot is completely thread-safe, and any mutations to it will not affect the actual object.

    Intent-based Object representations

    CoreStore is slowly moving to abstract object utilities based on usage intent. NSManageObject',CoreStoreObject,ObjectPublisher, andObjectSnapshotall conform to theObjectRepresentation` protocol, which allows conversion of each type to another:

    public protocol ObjectRepresentation {
        associatedtype ObjectType : CoreStore.DynamicObject
    
        func objectID() -> ObjectType.ObjectID
    
        func asPublisher(in dataStack: DataStack) -> ObjectPublisher<ObjectType>
        func asReadOnly(in dataStack: DataStack) -> ObjectType?
        func asEditable(in transaction: BaseDataTransaction) -> ObjectType?
        func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot<ObjectType>?
        func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot<ObjectType>?
    }
    

    ObjectMonitor being excluded in this family was intentional; its initialization is complex enough to be an API of its own.

    Source code(tar.gz)
    Source code(zip)
  • 6.3.0(Apr 1, 2019)

    • CoreStore now builds on Swift 5 and Xcode 10.2
    • SetupResult<T>, MigrationResult, and AsynchronousDataTransaction.Result<T> have all been converted into typealiases for Swift.Result<T, CoreStoreError>. The benefit is we can now use the utility methods on Swift.Result such as map(), mapError(), etc. Their Objective-C counterparts (CSSetupResult, etc.) remain available and can still be used as before.
    • Bunch of deprecated/obsoleted stuff deleted
    • CoreData iCloud support had been deprecated for a while now and CoreStore finally removes its support in this version. If you wish to continue using it please continue to use the 6.2.x versions but it will be unlikely to get bugfixes from here on out so please try to migrate your app's data as soon as possible (iOS and macOS already had this deprecated for years)
    Source code(tar.gz)
    Source code(zip)
  • 6.0.0(Jan 23, 2019)

    ⚠️This update will break current code. Make sure to read the changes below:

    Breaking changes

    • Minimum Deployment Version is raised to iOS 10, macOS 10.12, tvOS 10, watchOS 3
    • ICloudStore and ICloudStoreObservers are now officially deprecated (iCloud Core Data had been deprecated quite a long time ago).
    • Fetching and Querying methods now throws an error of type CoreStoreError.persistentStoreNotFound(DynamicObject.Type) when the specified entity does not exist in any storage. This is to distinguish difference between non-existing objects versus non-existing stores.
    // Before
    if let object = CoreStore.fetchOne(...) {
        // ...
    }
    else {
        // May be nil because `addStorage()` hasn't completed yet or because nothing really matches the query
    }
    
    // After
    do {
        if let object = try CoreStore.fetchOne(...) {
            // ...
        }
        else {
            // `addStorage()` has completed but nothing matches the query 
        }
    }
    catch {
        // method was called before `addStorage()` completed
    }
    

    If you are sure you won't encounter cases where fetches happen before a storage is added to the DataStack, simply add try! to your fetch*() and query*() method calls.

    Conveniences

    • CoreStoreObjects (as well as their PartialObject counterparts) now conform to CustomDebugStringConvertable by default.
    • CoreStoreObjects now assert on property names that possibly collide with reserved names such as description
    • CoreStoreObject properties can now be observed individually without the need for ObjectMonitors. The API is a bit similar to the standard KVO API:
    // NSManagedObject
    let observer = person.observe(\.friends, options: [.new, .old]) { (person, change) in
        // ...
    } 
    
    // CoreStoreObject
    let observer = person.friends.observe(options: [.new, .old]) { (person, change) in
       // ...
    }
    

    You may still use ObjectMonitors especially for observing multiple changes at once.

    • CoreStore now has its own Playgrounds file! You can find this at the top of the workspace (run pod try CoreStore if you don't have it locally). You will need to build the CoreStore iOS schema at least once to create the framework used by the Playgrounds.

    Improvements

    • Fixed queue assertion for UnsafeDataTransactions (https://github.com/JohnEstropia/CoreStore/pull/275)
    • ListMonitor access performance boost (https://github.com/JohnEstropia/CoreStore/pull/287, https://github.com/JohnEstropia/CoreStore/pull/288)
    • Added a new CoreStoreError.asynchronousMigrationRequired(URL) for cases when addStorageAndWait() is used together with .allowSynchronousLightweightMigration but migrations are only allowed asynchronously (#277)
    • CoreStore docs are now powered by jazzy
    • Fixed issue with InferredSchemaMappingProvider not respecting renaming identifiers (https://github.com/JohnEstropia/CoreStore/pull/301)
    Source code(tar.gz)
    Source code(zip)
  • 5.0.0(Dec 28, 2017)

    This release builds on Swift 4.

    Generic Clauses

    Most fetch and query clauses such as Where, OrderBy, etc. are now generic types. In effect, clauses using the previous fetching/querying calls need to indicate their generic type:

    var people = CoreStore.fetchAll(
        From<MyPersonEntity>(),
        Where<MyPersonEntity>("%K > %d", "age", 30),
        OrderBy<MyPersonEntity>(.ascending("name"))
    )
    

    This is quite repetitive. To make this even more convenient, CoreStore now has fetch/query builders.

    Fetch and Query Chain Builders

    var people = CoreStore.fetchAll(
        From<MyPersonEntity>()
            .where(format: "%K > %d", "age", 30)
            .orderBy(.ascending("name"))
    )
    

    This way the generic type is propagated onto subsequent clauses. But we can improve this further.

    Smart KeyPaths

    Using Swift 4's type-safe keypaths, we can make our fetch/query expressions even more solid.

    var people = CoreStore.fetchAll(
        From<MyPersonEntity>()
            .where(\.age > 30)
            .orderBy(.ascending(\.name))
    )
    

    All CoreStore API that accepts a string keypath now accepts these Smart Keypaths. (If I missed some, please post a github issue)

    Source code(tar.gz)
    Source code(zip)
  • 4.2.0(Oct 12, 2017)

  • 4.0.0(May 24, 2017)

    Upgrading from 3.x.x to 4.x.x

    Obsoleted

    • LegacySQLiteStore is now finally obsoleted in favor of SQLiteStore. For sqlite files that were created previously with LegacySQLiteStore, make sure to use the SQLiteStore.legacy(...) factory method to create an SQLiteStore that can load the file from the legacy file path.
    • SQLiteStore.init(...)'s mappingModelBundles argument is now obsolete. The new initializer accepts a migrationMappingProviders optional argument where explicit mapping sources are declared. For reference on how to do this, read on Custom migrations.

    Deprecated

    • DataStack.beginAsynchronous(...), DataStack.beginSynchronous(...), AsynchronousDataTransaction.commit(...), and SynchronousDataTransaction.commit(...) are now deprecated in favor of DataStack.perform(asynchronous:...) and DataStack.perform(synchronous:...) family of methods. These new perform(...) methods are auto-commit, meaning the transaction automatically calls commit() internally after the transction closure completes. To roll-back and cancel a transaction, call try transaction.cancel(). Read Saving and processing transactions for more details.

    Other Changes

    • ListMonitor.refetch(...) now works by recreating its internal NSFetchedResultsController. Previously refetch(...) would only apply new FetchClauses on top of previous fetches. Now all FetchClauses are required to be passed to refetch(...) each time it is called.
    • New important concepts on "Dynamic Models", "Schema", and "Schema Histories".
      • Dynamic Models (DynamicObject protocol): These are Core Data object types that any NSManagedObject or CoreStoreObjects conform to. (See Type-safe CoreStoreObjects)
      • Version Schema (DynamicSchema protocol): These types contain info for a single model version, as well as entities that belong to it. Currently supports XcodeDataModelSchema (.xcdatamodeld file), CoreStoreSchema, or UnsafeDataModelSchema. (See Migrations)
      • Schema History (SchemaHistory class): This is now the preferred way to express all models to the DataStack. This class contains info to all the DynamicSchema across multiple model versions. (See Migrations)
    Source code(tar.gz)
    Source code(zip)
  • 3.0.0(May 21, 2017)

    Upgrading from 2.x.x to 3.x.x

    Obsoleted

    • UnsageDataTransaction.internalContext was removed. Accessing the internal context (or more specifically, accessing context-level methods such as fetches) are now available through the FetchableSource and QueryableProtocol protocols, which are retrievable with NSManagedObject.fetchSource() and NSManagedObject.querySource() respectively. These protocols are implemented by DataStack and BaseDataTransaction.

    Deprecated Methods have been renamed to better fit the Swift 3 naming conventions.

    • entityDescriptionForType(_:)entityDescription(for:)
    • objectIDForURIRepresentation(_:)objectID(for:)
    • ImportableObject and ImportableUniqueObject protocol methods (and their variants) have been renamed. The old methods are still available, but will be removed in a future update.
      • shouldInsertFromImportSource(_:inTransaction:)shouldInsert(from:in:)
      • didInsertFromImportSource(_:inTransaction:)didInsert(from:in:)
      • shouldUpdateFromImportSource(_:inTransaction :)shouldUpdate(from:in:)
      • uniqueIDFromImportSource(_:inTransaction :)uniqueID(from:in:)
      • updateFromImportSource(_:inTransaction:)update(from:in:)

    Miscellaneous

    • APIs obsoleted from 2.0.0 have been removed.
    • CoreStore does not depend on GCDKit anymore, thanks to Swift 3's better Grand Central Dispatch API.
    • All enum cases are now lowercased
    • CoreStoreError now implements the new Swift CustomNSError protocol for better Objective-C bridging.
    • Some methods may emit warnings for unused return values. @discardableResult annotations have been set to better reflect the responsibility of API users to use/inspect return values.
    Source code(tar.gz)
    Source code(zip)
  • 2.0.0(May 21, 2017)

    Upgrading from 1.x.x to 2.x.x

    Obsoleted

    • AsynchronousDataTransaction.rollback() was removed. Undo and rollback functionality are now only allowed on UnsafeDataTransactions
    • DetachedDataTransaction was renamed to UnsafeDataTransaction
    • beginDetached() was renamed to beginUnsafe()
    • PersistentStoreResult was removed in favor of SetupResult<T>
    • SynchronousDataTransaction.commit() was renamed to SynchronousDataTransaction.commitAndWait()
    • From initializers that accepted NSURLs and NSPersistentStore were removed.

    Deprecated The following methods are still available, but will be removed in a future update.

    • add*Store(...) method variants. It is strongly recommended to convert to the new API. Refer to Local store) usage then use LegacySQLiteStore instead of SQLiteStore to maintain the old default file names and directory values
      • addInMemoryStoreAndWait(...)addStorageAndWait(InMemoryStore(...))
      • addSQLiteStoreAndWait(...)addStorageAndWait(LegacySQLiteStore(...))
      • addInMemoryStore(...)addStorage(InMemoryStore(...), ...)
      • addSQLiteStore(...)addStorage(LegacySQLiteStore(...), ...)
      • requiredMigrationsForSQLiteStore(...)requiredMigrationsForStorage(...)
      • upgradeSQLiteStoreIfNeeded(...)upgradeStorageIfNeeded(...)
      • The resetStoreOnModelMismatch: Bool argument for the methods above are now provided to the LegacySQLiteStore and SQLiteStore initializers as a LocalStorageOptions option set
    • NSError used to have a coreStoreErrorCode property that returns CoreStoreErrorCode enum, but all CoreStore errors are now guaranteed to be CoreStoreError enum type in swift, and CSError type on Objective-C.
    • CoreStoreLogger.handleError(...) was deprecated in favor of CoreStoreLogger.log(error:...). CoreStoreLogger may also implement CoreStoreLogger.abort(...), which is called just before CoreStore executes fatalError() due to critical runtime errors.
    Source code(tar.gz)
    Source code(zip)
Owner
John Estropia
John Estropia
Core Data Generator (CDG for short) is a framework for generation (using Sourcery) of Core Data entities from plain structs/classes/enums.

Core Data Generator Introduction Features Supported platforms Installation CDG Setup RepositoryType ModelType DataStoreVersion MigrationPolicy Basic U

Lotusflare 18 Sep 19, 2022
A powerful and elegant Core Data framework for Swift.

A powerful and elegant Core Data framework for Swift. Usage Beta version. New docs soon... Simple do that: let query = persistentContainer.viewContext

null 782 Nov 6, 2022
CloudCore is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift.

CloudCore CloudCore is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. Features Leveraging NSPersist

deeje cooley 123 Dec 31, 2022
JustPersist is the easiest and safest way to do persistence on iOS with Core Data support out of the box.

JustPersist JustPersist is the easiest and safest way to do persistence on iOS with Core Data support out of the box. It also allows you to migrate to

Just Eat 167 Mar 13, 2022
A minimalistic, thread safe, non-boilerplate and super easy to use version of Active Record on Core Data.

Skopelos A minimalistic, thread-safe, non-boilerplate and super easy to use version of Active Record on Core Data. Simply all you need for doing Core

Alberto De Bortoli 235 Sep 9, 2022
Super awesome Swift minion for Core Data (iOS, macOS, tvOS)

⚠️ Since this repository is going to be archived soon, I suggest migrating to NSPersistentContainer instead (available since iOS 10). For other conven

Marko Tadić 306 Sep 23, 2022
HitList is a Swift App shows the implementation of Core Data.

HitList HitList is a Swift App shows the implementation of Core Data. It is the demo app of Ray Wenderlich's tech blog. For details please reference G

Kushal Shingote 2 Dec 9, 2022
A type-safe, fluent Swift library for working with Core Data

Core Data Query Interface (CDQI) is a type-safe, fluent, intuitive library for working with Core Data in Swift. CDQI tremendously reduces the amount o

null 31 Oct 26, 2022
100% Swift Simple Boilerplate Free Core Data Stack. NSPersistentContainer

DATAStack helps you to alleviate the Core Data boilerplate. Now you can go to your AppDelegate remove all the Core Data related code and replace it wi

Nes 216 Jan 3, 2023
This project server as a demo for anyone who wishes to learn Core Data in Swift.

CoreDataDemo This project server as a demo for anyone who wishes to learn Core Data in Swift. The purpose of this project is to help someone new to Co

null 1 May 3, 2022
A type-safe, fluent Swift library for working with Core Data

Core Data Query Interface (CDQI) is a type-safe, fluent, intuitive library for working with Core Data in Swift. CDQI tremendously reduces the amount o

null 31 Oct 26, 2022
JSQCoreDataKit - A swifter Core Data stack

JSQCoreDataKit A swifter Core Data stack About This library aims to do the following: Encode Core Data best practices, so you don't have to think "is

Jesse Squires 596 Dec 3, 2022
QueryKit, a simple type-safe Core Data query language.

QueryKit QueryKit, a simple type-safe Core Data query language. Usage QuerySet<Person>(context, "Person")

QueryKit 1.5k Dec 20, 2022
A synchronization framework for Core Data.

Core Data Ensembles Author: Drew McCormack Created: 29th September, 2013 Last Updated: 15th February, 2017 Ensembles 2 is now available for purchase a

Drew McCormack 1.6k Dec 6, 2022
Core Data code generation

mogenerator Visit the project's pretty homepage. Here's mogenerator's elevator pitch: mogenerator is a command-line tool that, given an .xcdatamodel f

Wolf Rentzsch 3k Dec 30, 2022
Super Awesome Easy Fetching for Core Data!

MagicalRecord In software engineering, the active record pattern is a design pattern found in software that stores its data in relational databases. I

Magical Panda Software 10.9k Dec 29, 2022
A feature-light wrapper around Core Data that simplifies common database operations.

Introduction Core Data Dandy is a feature-light wrapper around Core Data that simplifies common database operations. Feature summary Initializes and m

Fuzz Productions 33 May 11, 2022
The Big Nerd Ranch Core Data Stack

BNR Core Data Stack The BNR Core Data Stack is a small Swift framework that makes it both easier and safer to use Core Data. A better fetched results

Big Nerd Ranch 561 Jul 29, 2022
Example repo of working with Core Data in a SwiftUI application

CoreData in SwiftUI This repository serves many purpose. But mostly so I can experiment with Core Data in SwiftUI, and also so I can use it show my ap

Donny Wals 4 Jan 30, 2022