{"id":954,"url":"https://github.com/albertodebortoli/Skopelos","last_synced_at":"2025-08-06T13:32:12.795Z","repository":{"id":10169331,"uuid":"64610091","full_name":"albertodebortoli/Skopelos","owner":"albertodebortoli","description":"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.","archived":false,"fork":false,"pushed_at":"2022-07-22T02:48:34.000Z","size":923,"stargazers_count":237,"open_issues_count":3,"forks_count":18,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-11-18T05:34:17.746Z","etag":null,"topics":["coredata","coredatastack","ios","persistence","swift"],"latest_commit_sha":null,"homepage":"http://albertodebortoli.com","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/albertodebortoli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-07-31T18:58:42.000Z","updated_at":"2024-05-25T15:34:46.000Z","dependencies_parsed_at":"2022-08-29T08:40:29.980Z","dependency_job_id":null,"html_url":"https://github.com/albertodebortoli/Skopelos","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albertodebortoli%2FSkopelos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albertodebortoli%2FSkopelos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albertodebortoli%2FSkopelos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albertodebortoli%2FSkopelos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/albertodebortoli","download_url":"https://codeload.github.com/albertodebortoli/Skopelos/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228905450,"owners_count":17989770,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coredata","coredatastack","ios","persistence","swift"],"created_at":"2024-01-05T20:15:35.472Z","updated_at":"2024-12-09T14:30:46.354Z","avatar_url":"https://github.com/albertodebortoli.png","language":"Swift","funding_links":[],"categories":["Core Data","Libs","Data Management [🔝](#readme)"],"sub_categories":["Linter","Data Management","Other free courses"],"readme":"# Skopelos\n\n![logo](./skopelos.png)\n\n[![Build Status](https://travis-ci.org/albertodebortoli/Skopelos.svg?branch=master)](https://travis-ci.org/albertodebortoli/Skopelos)\n[![Version](https://img.shields.io/cocoapods/v/Skopelos.svg?style=flat)](http://cocoapods.org/pods/Skopelos)\n[![License](https://img.shields.io/cocoapods/l/Skopelos.svg?style=flat)](http://cocoapods.org/pods/Skopelos)\n[![Platform](https://img.shields.io/cocoapods/p/Skopelos.svg?style=flat)](http://cocoapods.org/pods/Skopelos)\n\nA minimalistic, thread-safe, non-boilerplate and super easy to use version of Active Record on Core Data.\nSimply all you need for doing Core Data. Swift 4 flavour.\n\n[Objective-C version](https://github.com/albertodebortoli/Skiathos)\n\n## General notes\n\nThis component aims to have an extremely easy interface to introduce Core Data into your app with almost zero effort.\n\nThe design introduced here involves a few main components:\n\n- CoreDataStack\n- AppStateReactor\n- DALService (Data Access Layer)\n\n### CoreDataStack\n\nIf you have experience with Core Data, you might know that creating a stack is an annoying process full of pitfalls.\nThis component is responsible for the creation of the stack (in terms of chain of managed object contexts) using the design described [here](http://martiancraft.com/blog/2015/03/core-data-stack/) by Marcus Zarra.\n\n```\n      Managed Object Model \u003c------ Persistent Store Coordinator ------\u003e Persistent Store\n                                                ^\n                                                |\n                           Root Context (NSPrivateQueueConcurrencyType)\n                                                ^\n                                                |\n              ------------\u003e Main Context (NSMainQueueConcurrencyType) \u003c-------------\n              |                                 ^                                  |\n              |                                 |                                  |\n       Scratch Context                   Scratch Context                    Scratch Context\n(NSPrivateQueueConcurrencyType)   (NSPrivateQueueConcurrencyType)    (NSPrivateQueueConcurrencyType)\n```\n\nAn 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.\nOther 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.\nThe 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.\n\n### AppStateReactor\n\nYou 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.\n\n\n### DALService (Data Access Layer) / Skopelos\n\nIf 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.\nDatabases are all about readings and writings and for this reason our APIs are in the form of `read(statements: NSManagedObjectContext -\u003e Void)` and `writeSync(changes: NSManagedObjectContext -\u003e Void)`/`writeAsync(changes: NSManagedObjectContext -\u003e Void)`: 2 protocols providing a CQRS (Command and Query Responsibility Segregation) approach.\nRead 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.\nThe completion handler of the write methods calls the completion handler when the changes are saved back to the persistent store.\n\nIn other words, writings are always consistent in the main managed object context and eventual consistent in the persistent store.\nData are always available in the main managed object context.\n\n`Skopelos` is just a subclass of `DALService`, to give a nice name to the component. \n\n\n## How to use\n\nImport `Skopelos`.\n\nTo use this component, you could create a property of type `Skopelos` and instantiate it like so:\n\n```swift\nself.skopelos = Skopelos(sqliteStack: \"\u003c#ModelURL\u003e\")\n```\nor\n```swift\nself.skopelos = Skopelos(sqliteStack: \"\u003c#ModelURL\u003e\", securityApplicationGroupIdentifier: \"\u003c#GroupID\u003e\")\n\n```\nor\n```swift\nself.skopelos = Skopelos(inMemoryStack: \"\u003c#ModelURL\u003e\")\n```\n\nN.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.\n\nWhile 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.\n\nYou could inherit from `Skopelos` to:\n- wrap it into an interface that is specific to you use-case\n- override `handleError(_error: NSError)` to perform specific actions whenever an error is encountered\n\nHere is an example:\n\n```swift\nprotocol SkopelosClientDelegate: class {\n    func handle(_ error: NSError)\n}\n\nclass SkopelosClient: Skopelos {\n\n    static let modelURL = Bundle(identifier: \"\u003c#com.mydomain.myapp\u003e\").url(forResource: \"\u003c#DataModel\u003e\", withExtension: \"momd\")!\n\n    weak var delegate: SkopelosClientDelegate?\n\n    class func sqliteStack() -\u003e Skopelos {\n        return Skopelos(sqliteStack: modelURL)\n    }\n\n    class func inMemoryStack() -\u003e Skopelos {\n        return Skopelos(inMemoryStack: modelURL)\n    }\n\n    override func handleError(_ error: NSError) {\n        DispatchQueue.main.async {\n            self.delegate?.handle(error)\n        }\n    }\n}\n```\n\n### Readings and writings\n\nSpeaking of readings and writings, let's do now a comparison between some standard Core Data code and code written with Skopelos.\n\nStandard Core Data reading:\n\n```objc\n__block NSArray *results = nil;\n\nNSManagedObjectContext *context = ...;\n[context performBlockAndWait:^{\n\n    NSFetchRequest *request = [[NSFetchRequest alloc] init];\n    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:NSStringFromClass(User)\n    inManagedObjectContext:context];\n    [request setEntity:entityDescription];\n\n    NSError *error;\n    results = [context executeFetchRequest:request error:\u0026error];\n}];\n\nreturn results;\n```\n\nStandard Core Data writing:\n\n```objc\nNSManagedObjectContext *context = ...;\n[context performBlockAndWait:^{\n\n    User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass(User)\n    inManagedObjectContext:context];\n    user.firstname = @\"John\";\n    user.lastname = @\"Doe\";\n\n    NSError *error;\n    [context save:\u0026error];\n    if (!error)\n    {\n        // continue to save back to the store\n    }\n}];\n\n```\n\nSkopelos reading: \n\n```swift\nskopelosClient.read { context in\n    let users = User.SK_all(context)\n    print(users)\n}\n```\n\nSkopelos writing:\n\n```swift\n// Sync\nskopelosClient.writeSync { context in\n    let user = User.SK_create(context)\n    user.firstname = \"John\"\n    user.lastname = \"Doe\"\n}\n\nskopelosClient.writeSync({ context in\n    let user = User.SK_create(context)\n    user.firstname = \"John\"\n    user.lastname = \"Doe\"\n    }, completion: { (error: NSError?) in\n        // changes are saved to the persistent store\n})\n\n// Async\nskopelosClient.writeAsync { context in\n    let user = User.SK_create(context)\n    user.firstname = \"John\"\n    user.lastname = \"Doe\"\n}\n\nskopelosClient.writeAsync({ context in\n    let user = User.SK_create(context)\n    user.firstname = \"John\"\n    user.lastname = \"Doe\"\n}, completion: { (error: NSError?) in\n    // changes are saved to the persistent store\n})\n```\n\nSkopelos also supports chaining:\n\n```swift\nskopelosClient.writeSync { context in\n    user = User.SK_create(context)\n    user.firstname = \"John\"\n    user.lastname = \"Doe\"\n}.writeSync { context in\n    if let userInContext = user.SK_inContext(context) {\n        userInContext.SK_remove(context)\n    }\n}.read { context in\n    let users = User.SK_all(context)\n    print(users)\n}\n```\n\nThe `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:\n\n```swift\nstatic func SK_create(context: NSManagedObjectContext) -\u003e Self\nstatic func SK_numberOfEntities(context: NSManagedObjectContext) -\u003e Int\nfunc SK_remove(context: NSManagedObjectContext)\nstatic func SK_removeAll(context: NSManagedObjectContext)\nstatic func SK_all(context: NSManagedObjectContext) -\u003e [Self]\nstatic func SK_all(predicate: NSPredicate, context:NSManagedObjectContext) -\u003e [Self]\nstatic func SK_first(context: NSManagedObjectContext) -\u003e Self?\n```\n\nMind the usage of `SK_inContext:` to retrieve an object in different read/write blocks (same read blocks are safe).\n\n\n## Thread-safety notes\n\nAll the accesses to the persistence layer done via a `DALService` instance are guaranteed to be thread-safe.\n\nIt 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).\n\nThis 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).\nTherefore, 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`).\n\n\n## Clients\n\nSkopelos is used in production in the following products:\n\n- [Just Eat](https://itunes.apple.com/gb/app/just-eat-food-delivery/id566347057?mt=8)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falbertodebortoli%2FSkopelos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falbertodebortoli%2FSkopelos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falbertodebortoli%2FSkopelos/lists"}