SkillAgentSearch skills...

Cadmium

A Swift framework that wraps CoreData, hides context complexity, and helps facilitate best practices.

Install / Use

/learn @jmfieldman/Cadmium
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Cadmium

Cadmium is a Core Data framework for Swift that enforces best practices and raises exceptions for common Core Data pitfalls exactly where you make them.

Cadmium was written as a reaction to the complexity of dealing with multiple managed object contexts for standard database-like use cases. It is still important to understand what a managed object context is and how they are used, but for typical CRUD-style usage of Core Data it is a complete nuisance.

With Cadmium, the user never sees a NSManagedObjectContext or derived class. You interact only with argument-less transactions, and object fetch/manipulation tasks. The contexts are managed in the background, which makes Core Data feel more like Realm.

Design Goals

  • Create a minimalist/concise framework API that provides for most Core Data use cases and guides the user towards best practices.
  • Aggressively protect the user from performing common Core Data pitfalls, and raise exceptions immediately on the offending statement rather than waiting for a context save event.

Here's an example of a Cadmium transaction that gives all of your employee objects a raise:

Cd.transact {
    try! Cd.objects(Employee.self).fetch().forEach {
        $0.salary += 10000
    }
}

You might notice a few things:

  • Transaction usage is dead-simple. You do not declare any parameters for use inside the block.
  • You never have to reference the managed object context, we manage it for you.
  • The changes are committed automatically upon completion (you can disable this.)

What Cadmium is Not

Cadmium is not designed to be a 100% complete wrapper around Core Data. Some of the much more advanced Core Data features are hidden behind the Cadmium API. If you are creating an enterprise-level application that requires meticulous manipulation of Core Data stores and contexts to optimize heavy lifting, then Cadmium is not for you.

Cadmium is for you if want a smart wrapper that vastly simplifies most Core Data tasks and warns you immediately when you inadvertently manipulate data in a way you shouldn't.

Installing

You can install Cadmium by adding it to your CocoaPods Podfile:

pod 'Cadmium'

Or you can use a variety of ways to include the Cadmium.framework file from this project into your own.

Swift Version Support

Swift 3.1: Use Cadmium 1.1

Swift 3.0: Use Cadmium 1.0

Swift 2.3: Use Cadmium 0.13.x

Swift 2.2: Use Cadmium 0.12.x

Cocoapods:

pod 'Cadmium', '~> 1.2'  # Swift 4.0
pod 'Cadmium', '~> 1.1'  # Swift 3.1 
pod 'Cadmium', '~> 1.0'  # Swift 3.0
pod 'Cadmium', '~> 0.13' # Swift 2.3
pod 'Cadmium', '~> 0.12' # Swift 2.2

How to Use

Context Architecture

Cadmium uses the same basic context architecture as CoreStore, with a root save context running on a private queue that has one read-only child context on the main queue and any number of writeable child contexts running on background queues.

Cadmium Core Data Architecture

This means that your main thread will never bog down on write transactions, and will only be used to merge changes (in memory) and updating any UI elements dependent on your data.

It also means that you cannot initiate modifications to managed objects on the main thread! All of your write operations must exist inside transactions that occur in background threads. You will need to design your app to support the idea of asynchronous write operations, which is what you should be doing when it comes to database modification.

Managed Object Model

The creation and use of the managed object model is very similar to typical Core Data flow. Create your managed object model as usual, and generate the corresponding NSManagedObject classes. Then, simply change the hierarchy so that your class implementations derive from CdManagedObject instead of NSManagedObject.

CdManagedObject is a child class of NSManagedObject.

Initialization

Set up Cadmium with a single initialization call:

do {
    try Cd.initWithSQLStore(momdInbundleID: nil,
                            momdName:       "MyObjectModel.momd",
                            sqliteFilename: "MyDB.sqlite",
                            options:        nil /* Optional */)
} catch let error {
    print("\(error)")
}

This loads the object model, sets up the persistent store coordinator, and initializes important contexts.

If your object model is in a framework (not your main bundle), you'll have to pass the framework's bundle identifier to the first argument.

The options argument flows through to the options passed in addPersistentStoreWithType: on the NSPersistentStoreCoordinator.

You can pass nil to the sqliteFilename parameter to create an NSInMemoryStoreType database.

Querying

Cadmium offers a chained query mechanism. This can be used to query objects from the main thread (for read-only purposes), or from inside a transaction.

Querying starts with Cd.objects(..) and looks like this:

do {
    for employee in try Cd.objects(Employee.self)
                          .filter("name = %@", someName)
                          .sorted("salary", ascending: true)
                          // See CdFetchRequest for more functions
                          .fetch() {
        /* Do something */
        print("Employee name: \(employee.name)")
    }
} catch let error {
    print("\(error)")
}

You begin by passing the managed object type into the parameter for Cd.objects(..). This constructs a CdFetchRequest for managed objects of that type.

Chain in as many filter/sort/modification calls as you want, and finalize with fetch() or fetchOne(). fetch() returns an array of objects, and fetchOne() returns a single optional object (nil if none were found matching the filter).

Transactions

You can only initiate changes to your data from inside of a transaction. You can initiate a transaction using either:

Cd.transact {
    //...
}
Cd.transactAndWait {
    //...
}

Cd.transact performs the transaction asynchronously (the calling thread continues while the work in the transaction is performed). Cd.transactAndWait performs the transaction synchronously (it will block the calling thread until the transaction is complete.)

To ensure best practices and avoid potential deadlocks, you are not allowed to call Cd.transactAndWait from the main thread (this will raise an exception.)

Implicit Transaction Commit

When a transaction completes, the transaction context automatically commits any changes you made to the data store. For most transactions this means you do not need to call any additional commit/save command.

If you want to turn off the implicit commit for a transaction (e.g. to perform a rollback and ignore any changes made), you can call Cd.cancelImplicitCommit() from inside the transaction. A typical use case would look like:

Cd.transact {

    modifyThings()

    if someErrorOccurred {
        Cd.cancelImplicitCommit()
        return
    }

    moreActions()
}

You can also force a commit mid-transaction by calling Cd.commit(). You may want to do this during long transactions when you want to save changes before possibly returning with a cancelled implicit commit. A use case might look like:

Cd.transact {

    modifyThingsStepOne()
    Cd.commit() //changes in modifyThingsStepOne() cannot be rolled back!

    modifyThingsStepTwo()

    if someErrorOccurred {
        Cd.cancelImplicitCommit()
        return
    }

    moreActions()
}

Forced Serial Transactions

NOTE: Advanced Feature

Core Data, and Cadmium, are asynchronous APIs by nature. You generally initiate fetches and modify data asynchronously from the main thread. This tight coupling with asynchronous behavior may be detrimental if you find that the context modifications you perform often conflict with each other.

For example, take the following transaction that might occur when the user taps a button to visit a place:

Cd.transact {
    if let place = try! Cd.objects(Place.self).filter("id = %@", myID).fetchOne() {
        place.visits += 1
    }
}

What if the user spams the visit button? Because the transactions occur in separate context queues, it's not 100% guaranteed that place.visits will increment serially. There is a remote possibility that a race condition will cause two of these contexts to see place.visits as the same value before incrementing.

To help resolve this problem and ensure that transactions are executed serially, you may pass an optional serial parameter to your transactions:

Cd.transact(serial: true) {
    if let place = try! Cd.objects(Place.self).filter("id = %@", myID).fetchOne() {
        place.visits += 1
    }
}

This guarantees that your transactions will occur serially (even waiting for the finalized context save) before proceeding to the next one -- thus your transactions can be considered atomic.

Note that this atomic behavior is limited to the top-most transaction. Transactions-inside-transactions are not executed on the serial queue to prevent deadlocks.

/* In this odd case, the inside transactAndWait will ignore the serial parameter, since it is already
   inside a transaction.  The prevents deadlocks waiting on the serial queue. */

Cd.transact(serial: true) {
    Cd.transactAndWait(serial: true) {
    }
}

It is annoying to pass the serial parameter every time you want this behavior, especially if you want in most of the time. If you want your transactions to be serial by default, pass true to the serialTX parameter in the Cd.init function:

try Cd.initWithSQLStore(momdInbundleID: "org.fieldman.CadmiumTests",
             
View on GitHub
GitHub Stars123
CategoryDevelopment
Updated1y ago
Forks4

Languages

Swift

Security Score

80/100

Audited on Feb 28, 2025

No findings