A minimalistic, thread safe, non-boilerplate and super easy to use version of Active Record on Core Data.

Last update: May 11, 2022

Skopelos

logo

Build Status Version License Platform

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 Data. Swift 4 flavour.

Objective-C version

General notes

This component aims to have an extremely easy interface to introduce Core Data into your app with almost zero effort.

The design introduced here involves a few main components:

  • CoreDataStack
  • AppStateReactor
  • DALService (Data Access Layer)

CoreDataStack

If you have experience with Core Data, you might know that creating a stack is an annoying process full of pitfalls. This component is responsible for the creation of the stack (in terms of chain of managed object contexts) using the design described here by Marcus Zarra.

      Managed Object Model <------ Persistent Store Coordinator ------> Persistent Store
                                                ^
                                                |
                           Root Context (NSPrivateQueueConcurrencyType)
                                                ^
                                                |
              ------------> Main Context (NSMainQueueConcurrencyType) <-------------
              |                                 ^                                  |
              |                                 |                                  |
       Scratch Context                   Scratch Context                    Scratch Context
(NSPrivateQueueConcurrencyType)   (NSPrivateQueueConcurrencyType)    (NSPrivateQueueConcurrencyType)

An important difference from Magical Record, or other third-party libraries, is that the savings always go in one direction, from scratch contexts down (up direction in the above diagram) to the persistent store. Other components allow you to create scratch contexts that have the private context as parent and this causes the main context not to be updated or to be updated via notifications to merge the context. The main context should be the source of truth and it is tied the UI: having a much simpler approach helps to create a system easier to reason about.

AppStateReactor

You should ignore this one. It sits in the CoreDataStack and takes care of saving the in-flight changes back to disk if the app goes to background, loses focus or is about to be terminated. It's a silent friend who takes care of us.

DALService (Data Access Layer) / Skopelos

If you have experience with Core Data, you might also know that most of the operations are repetitive and that we usually call performBlock/performBlockAndWait on a context providing a block that eventually will call save: on that context as last statement. Databases are all about readings and writings and for this reason our APIs are in the form of read(statements: NSManagedObjectContext -> Void) and writeSync(changes: NSManagedObjectContext -> Void)/writeAsync(changes: NSManagedObjectContext -> Void): 2 protocols providing a CQRS (Command and Query Responsibility Segregation) approach. Read blocks will be executed on the main context (as it's considered to be the single source of truth). Write blocks are executed on a scratch context which is saved at the end; changes are eventually saved asynchronously back to the persistent store without blocking the main thread. The completion handler of the write methods calls the completion handler when the changes are saved back to the persistent store.

In other words, writings are always consistent in the main managed object context and eventual consistent in the persistent store. Data are always available in the main managed object context.

Skopelos is just a subclass of DALService, to give a nice name to the component.

How to use

Import Skopelos.

To use this component, you could create a property of type Skopelos and instantiate it like so:

") ">
self.skopelos = Skopelos(sqliteStack: "<#ModelURL>")

or

", securityApplicationGroupIdentifier: "<#GroupID>") ">
self.skopelos = Skopelos(sqliteStack: "<#ModelURL>", securityApplicationGroupIdentifier: "<#GroupID>")

or

") ">
self.skopelos = Skopelos(inMemoryStack: "<#ModelURL>")

N.B. All the above methods also accept an extra optional argument allowsConcurrentWritings (which defaults to false) to allow using a dedicated scratch context per writing operation. For simple applications, reusing the same scratch context (i.e. using the default value) on writings helps avoiding race conditions when the changes are pushed to the main context.

While it would be acceptable to treat Skopelos as a singleton, it's always best to not use such patter but rather explicitly instantiate a single instance and inject it to parts of the app via dependency injection. Generally speaking, we don't like singletons. They are not testable by nature, clients don't have control over the lifecycle of the object and they break some principles. For these reasons, the library comes free of singletons.

You could inherit from Skopelos to:

  • wrap it into an interface that is specific to you use-case
  • override handleError(_error: NSError) to perform specific actions whenever an error is encountered

Here is an example:

").url(forResource: "<#DataModel>", withExtension: "momd")! weak var delegate: SkopelosClientDelegate? class func sqliteStack() -> Skopelos { return Skopelos(sqliteStack: modelURL) } class func inMemoryStack() -> Skopelos { return Skopelos(inMemoryStack: modelURL) } override func handleError(_ error: NSError) { DispatchQueue.main.async { self.delegate?.handle(error) } } } ">
protocol SkopelosClientDelegate: class {
    func handle(_ error: NSError)
}

class SkopelosClient: Skopelos {

    static let modelURL = Bundle(identifier: "<#com.mydomain.myapp>").url(forResource: "<#DataModel>", withExtension: "momd")!

    weak var delegate: SkopelosClientDelegate?

    class func sqliteStack() -> Skopelos {
        return Skopelos(sqliteStack: modelURL)
    }

    class func inMemoryStack() -> Skopelos {
        return Skopelos(inMemoryStack: modelURL)
    }

    override func handleError(_ error: NSError) {
        DispatchQueue.main.async {
            self.delegate?.handle(error)
        }
    }
}

Readings and writings

Speaking of readings and writings, let's do now a comparison between some standard Core Data code and code written with Skopelos.

Standard Core Data reading:

__block NSArray *results = nil;

NSManagedObjectContext *context = ...;
[context performBlockAndWait:^{

    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:NSStringFromClass(User)
    inManagedObjectContext:context];
    [request setEntity:entityDescription];

    NSError *error;
    results = [context executeFetchRequest:request error:&error];
}];

return results;

Standard Core Data writing:

NSManagedObjectContext *context = ...;
[context performBlockAndWait:^{

    User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass(User)
    inManagedObjectContext:context];
    user.firstname = @"John";
    user.lastname = @"Doe";

    NSError *error;
    [context save:&error];
    if (!error)
    {
        // continue to save back to the store
    }
}];

Skopelos reading:

skopelosClient.read { context in
    let users = User.SK_all(context)
    print(users)
}

Skopelos writing:

// Sync
skopelosClient.writeSync { context in
    let user = User.SK_create(context)
    user.firstname = "John"
    user.lastname = "Doe"
}

skopelosClient.writeSync({ context in
    let user = User.SK_create(context)
    user.firstname = "John"
    user.lastname = "Doe"
    }, completion: { (error: NSError?) in
        // changes are saved to the persistent store
})

// Async
skopelosClient.writeAsync { context in
    let user = User.SK_create(context)
    user.firstname = "John"
    user.lastname = "Doe"
}

skopelosClient.writeAsync({ context in
    let user = User.SK_create(context)
    user.firstname = "John"
    user.lastname = "Doe"
}, completion: { (error: NSError?) in
    // changes are saved to the persistent store
})

Skopelos also supports chaining:

skopelosClient.writeSync { context in
    user = User.SK_create(context)
    user.firstname = "John"
    user.lastname = "Doe"
}.writeSync { context in
    if let userInContext = user.SK_inContext(context) {
        userInContext.SK_remove(context)
    }
}.read { context in
    let users = User.SK_all(context)
    print(users)
}

The NSManagedObject category provides CRUD methods always explicit on the context. The context passed as parameter should be the one received in the read or write block. You should always use these methods from within read/write blocks. Main methods are:

static func SK_create(context: NSManagedObjectContext) -> Self
static func SK_numberOfEntities(context: NSManagedObjectContext) -> Int
func SK_remove(context: NSManagedObjectContext)
static func SK_removeAll(context: NSManagedObjectContext)
static func SK_all(context: NSManagedObjectContext) -> [Self]
static func SK_all(predicate: NSPredicate, context:NSManagedObjectContext) -> [Self]
static func SK_first(context: NSManagedObjectContext) -> Self?

Mind the usage of SK_inContext: to retrieve an object in different read/write blocks (same read blocks are safe).

Thread-safety notes

All the accesses to the persistence layer done via a DALService instance are guaranteed to be thread-safe.

It is highly suggested to enable the flag -com.apple.CoreData.ConcurrencyDebug 1 in your project to make sure that you don't misuse Core Data in terms of threading and concurrency (by accessing managed objects from different threads and similar errors).

This component doesn't aim to introduce interfaces with the goal of hiding the concept of ManagedObjectContext: it would open up the doors to threading issues in clients' code as developers should be responsible to check for the type of the calling thread at some level (that would be ignoring the benefits that Core Data gives us). Therefore, our design forces to make all the readings and writings via the DALService and the ManagedObject category methods are intended to always be explicit on the context (e.g. SK_create).

Clients

Skopelos is used in production in the following products:

GitHub

https://github.com/albertodebortoli/Skopelos
Comments
  • 1. Update README.md terminology for managed object contexts

    This PR replaces occurrences of 'slave' with 'child' in the README.

    Hi @albertodebortoli ! πŸ˜„

    The "slave/master" terminology can be unwelcoming for some people. There are precedents for this naming scheme, but also precedents to change it to avoid potentially offensive language.

    Also, Core Data documentation uses "parent/child" nomenclature β€” so this makes your docs more consistent with those.

    Hope you'll accept this PR! 😊

    Reviewed by jessesquires at 2016-08-22 15:08
  • 2. Swift 3 version?

    Hello! Just wanted to play with CoreData, but xCode can't let me import Swift-Legacy libraries in project... Any news about swift3 version of Skopelos?

    Reviewed by Luccifer at 2016-09-13 23:59
  • 3. Add persistent store and migration (if needed) off the main thread re…

    This PR moves the initialization off the main thread regardless of the provided callback. This should protect the caller from having the app killed by the WatchDog if the SQLite is too big to migrate in few seconds.

    Reviewed by gigisommo at 2019-01-21 10:30
  • 4. Double allocating main and root moc?

    Hi, first of all, great code you provide.

    By looking at the code at CoreDataStack.swift class, init which calls the initialize method at the end. Both of method creates the main and root contexts. Perhaps, designated initializer initializes the global variable, but I don't understand the reason initialize method calls again. Is there any reason?

    Reviewed by YoonLee at 2018-12-28 09:51
  • 5. Add shouldAddStoreAsynchronously flag to CoreDataStack and Skopelos

    • [x] Make explicit the possibility to add the store in background
    • [x] Use the property naming used by NSPersistentStoreDescription API
    • [x] Refactor and expand unit tests
    Reviewed by albertodebortoli at 2019-02-03 20:40
  • 6. Improve error handling

    • [x] Rename handle(error: NSError) to handleError(_ error: NSError)
    • [x] Call handleError in case the stack fails pushing/saving the changes to the main context
    Reviewed by albertodebortoli at 2019-01-28 18:10
  • 7. Use a single slave context to avoid undesired concurrent updates

    Skopelos has been developed with simplicity in mind. Concurrency is hard and allowing multiple scratch contexts to perform writing operations can cause unexpected behaviours when saving them to the main context and ultimately to the persistent store. For instance, performing the same async operation twice could save obj1 in the slaveContext1, obj2 in the slaveContext2 ultimately causing the object (i.e. the instances obj1 and obj2) to be duplicated in the persistent store even though there should be only one. Having a single context for the writings still allows nested writings. Nested readings were already allowed.

    Reviewed by albertodebortoli at 2017-11-22 22:01
  • 8. Bump cocoapods-downloader from 1.2.2 to 1.6.3

    Bumps cocoapods-downloader from 1.2.2 to 1.6.3.

    Release notes

    Sourced from cocoapods-downloader's releases.

    1.6.3

    Enhancements
    • None.
    Bug Fixes
    • None.

    1.6.2

    Enhancements
    • None.
    Bug Fixes
    • None.

    1.6.1

    Enhancements
    • None.
    Bug Fixes
    • None.

    1.6.0

    Enhancements
    • None.
    Bug Fixes
    • Adds a check for command injections in the input for hg and git.
      orta #124

    1.5.1

    Enhancements
    • None.
    Bug Fixes
    • Fix "can't modify frozen string" errors when pods are integrated using the branch option
      buju77 #10920

    1.5.0

    ... (truncated)

    Changelog

    Sourced from cocoapods-downloader's changelog.

    1.6.3 (2022-04-01)

    Enhancements
    • None.
    Bug Fixes
    • None.

    1.6.2 (2022-03-28)

    Enhancements
    • None.
    Bug Fixes
    • None.

    1.6.1 (2022-03-23)

    Enhancements
    • None.
    Bug Fixes
    • None.

    1.6.0 (2022-03-22)

    Enhancements
    • None.
    Bug Fixes
    • Adds a check for command injections in the input for hg and git.
      orta #124

    1.5.1 (2021-09-07)

    Enhancements
    • None.

    ... (truncated)

    Commits
    • c03e2ed Release 1.6.3
    • f75bccc Disable Bazaar tests due to macOS 12.3 not including python2
    • 52a0d54 Merge pull request #128 from CocoaPods/validate_before_dl
    • d27c983 Ensure that the git pre-processor doesn't accidentally bail also
    • 3adfe1f [CHANGELOG] Add empty Master section
    • 591167a Release 1.6.2
    • d2564c3 Merge pull request #127 from CocoaPods/validate_before_dl
    • 99fec61 Switches where we check for invalid input, to move it inside the download fun...
    • 96679f2 [CHANGELOG] Add empty Master section
    • 3a7c54b Release 1.6.1
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    Reviewed by dependabot[bot] at 2022-04-05 22:21
  • 9. How to use NSBatchUpdateRequest

    Here is the sample code that I am using:

    let dataStore = SkopelosClient.sqliteStack()
    dataStore.writeSync({ context in
                
                // Update Records
                let batchRequest = NSBatchUpdateRequest(
                    entityName: "User"
                )
                batchRequest.propertiesToUpdate = ["lastname": "T123T"]
                batchRequest.resultType = .updatedObjectsCountResultType
                
                do {
                    let result = try? context.execute(batchRequest)
                    print("Success")
                } catch {
                    print("Error")
                }
                
    }).read({ context in
                let users = User.SK_all(context)
                print(users)
    })
    

    I am trying to batch update using Skopelos library. But its not printing log Success or Error. Breakpoint is hitting at line let result = try? context.execute(batchRequest) but not moving ahead.

    Any help will be appriciated.

    Reviewed by shardul89 at 2019-05-01 09:17
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

Jun 21, 2022
JSON to Core Data and back. Swift Core Data Sync.
JSON to Core Data and back. Swift Core Data Sync.

Notice: Sync was supported from it's creation back in 2014 until March 2021 Moving forward I won't be able to support this project since I'm no longer

Jun 8, 2022
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 (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

Feb 7, 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

Jan 29, 2022
QueryKit, a simple type-safe Core Data query language.
QueryKit, a simple type-safe Core Data query language.

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

Jun 16, 2022
A type-safe, fluent Swift library for working with Core Data
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

Jun 27, 2020
A type-safe, fluent Swift library for working with Core Data
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

Jun 27, 2020
DataKernel is a minimalistic wrapper around CoreData stack to ease persistence operations.

DataKernel What is DataKernel? DataKernel is a minimalistic wrapper around CoreData stack to ease persistence operations. It is heavily inspired by Su

Jun 2, 2022
A powerful and elegant Core Data framework for Swift.
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

May 23, 2022
CloudCore is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift.
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

Jun 23, 2022
Unleashing the real power of Core Data with the elegance and safety of Swift
 Unleashing the real power of Core Data with the elegance and safety of Swift

Unleashing the real power of Core Data with the elegance and safety of Swift Dependency managers Contact Swift 5.4: iOS 11+ / macOS 10.13+ / watchOS 4

Jun 25, 2022
JustPersist is the easiest and safest way to do persistence on iOS with Core Data support out of the box.
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

Mar 13, 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

Jun 17, 2022
HitList is a Swift App shows the implementation of Core Data.
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

Dec 17, 2021
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

Jun 12, 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

Jun 21, 2022
A feature-light wrapper around Core Data that simplifies common database operations.
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

May 11, 2022
The Big Nerd Ranch Core Data Stack
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

Jun 6, 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

Jan 30, 2022