• Stars
    star
    236
  • Rank 170,480 (Top 4 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 8 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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

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:

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:

More Repositories

1

GoldRaccoon

The iOS component to connect to a FTP service and perform the operations you need. http://albertodebortoli.github.io/GoldRaccoon/
Objective-C
159
star
2

Promis

The easiest Future and Promises framework in Swift. No magic. No boilerplate.
Swift
110
star
3

Stateful

A minimalistic, thread-safe, non-boilerplate and super easy to use state machine in Swift.
Swift
96
star
4

ADBIndexedTableView

Indexed UITableView using first letter objects property.
Objective-C
59
star
5

ADBActors

Simple concept of Actor Model in Objective-C based on the idea of Valletta Ventures Actors library.
Objective-C
58
star
6

ADBBackgroundCells

ADBBackgroundCells allow lazy loading for UITableViewCell objects performing a long time job in background without blocking the UI.
Objective-C
42
star
7

Bitlyzer

Class to shorten URLs with Bit.ly on iOS (both block based and delegate based using ARC)
Objective-C
41
star
8

ADBDownloadManager

A download manager for iOS. Actually, all that you need to download files without any external library.
Objective-C
33
star
9

ADBGridView

ADBGridView inherits from UITableView and is populated with ADBImageViews (https://github.com/albertodebortoli/ADBImageView). Number of images for row (cells) can be customized. UITableView is inherited to use cell reuse facility.
Objective-C
24
star
10

Skiathos

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. Objective-C flavour.
Objective-C
19
star
11

ModulePods

Toolbar app to easily run common CocoaPods commands
Swift
12
star
12

rubiks-cube-solution

The simplest, easiest and quickest way to learn how to solve the Rubik's Cube.
10
star
13

ADBStateMachine

A proper thread-safe state machine for Objective-C.
Objective-C
10
star
14

WWDC-2016-Apple-Logo

Swift
6
star
15

ImageAnalysisFilters

A simple C++ project for applying filters to raw images via command line.
C++
4
star
16

Introspecta

Utilities for introspection based on Objective-C runtime
Objective-C
4
star
17

ADBImageView

Asynchronous Image View for iOS providing placeholder, activity indicator, gestures and caching. Delegation and ARC based.
Objective-C
4
star
18

ADBCategories

Useful categories for iOS with ARC
Objective-C
3
star
19

Uncrustify-ObjC

Uncrustify configuration file for Objective-C files.
2
star
20

ADBReasonableTextView

A UITextView replacement with reasonable delegate methods.
Objective-C
2
star
21

iOSLab-DigitalAccademia-2012

Digital Accademia iOS Lab held in October 2012
Objective-C
2
star
22

iVIP

iVIP is an Xcode template project that allows quick creation of applications focused on a single, maybe famous, person.
Objective-C
2
star
23

EC2macConnector

EC2macConnector is a CLI tool that helps connect to EC2 Mac instances easily.
Swift
2
star
24

BMYSmartFeed

Facebook-style iOS feed architecture
Objective-C
2
star
25

the_coding_love

Simple iOS third-party reader for the_coding_love(); that shows GIFs when they are fully loaded.
Objective-C
1
star
26

iHarmonyDB

The DB powering iHarmony
1
star
27

ADBLanguageManager

A reusable localization manager class for iOS by Toni Sala and Alberto De Bortoli
Objective-C
1
star
28

LBDelegateMatrioska

Multiple delegates by using NSProxy
Objective-C
1
star
29

RuzzleSolver

A quick and dirty Ruzzle solver in Ruby using Ukkonen's suffix tree algorithm.
Ruby
1
star
30

Xcode-4-Themes

Xcode 4 Themes I use
1
star
31

ADBObjectLocker

Prototype. Idea. Maybe pointless. Maybe just cool mental gymnastic. Maybe I should just drink less beer. Class to handle lock on specific methods for specific instances.
Objective-C
1
star