Skip to content

Instantly share code, notes, and snippets.

@ashevin
Created October 24, 2017 08:47
Show Gist options
  • Save ashevin/c12daa7ce1ecefc846f3d0ddff64d853 to your computer and use it in GitHub Desktop.
Save ashevin/c12daa7ce1ecefc846f3d0ddff64d853 to your computer and use it in GitHub Desktop.
CoreDataManager
//
// CoreDataManager.swift
// CoreDataManager
//
// Created by Avi Shevin on 23/10/2017.
// Copyright © 2017 Avi Shevin. All rights reserved.
//
import Foundation
import CoreData
typealias CoreDataManagerUpdateBlock =
(NSManagedObjectContext, inout Bool) -> ()
typealias CoreDataManagerQueryBlock = (NSManagedObjectContext) -> ()
class CoreDataManager {
let viewContext: ReadOnlyMOC
private let coordinator: NSPersistentStoreCoordinator
private let queue = DispatchQueue(label: "cdm.queue")
private var token: AnyObject? = nil
convenience init?(name: String) {
self.init(name: name, storeType: NSSQLiteStoreType)
}
init?(name: String, storeType: String) {
guard let model = CoreDataManager.model(for: name) else {
return nil
}
let storeFile = FileManager.default.urls(for: .documentDirectory, in: .allDomainsMask)[0]
.appendingPathComponent(name)
.appendingPathExtension("sqlite")
do {
let options: [String: Any] = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true,
NSSQLitePragmasOption: ["journal_mode": "WAL"]
]
coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
try coordinator.addPersistentStore(ofType: storeType,
configurationName: nil,
at: storeFile,
options: storeType == NSSQLiteStoreType
? options
: nil)
}
catch {
return nil
}
viewContext = ReadOnlyMOC(concurrencyType: .mainQueueConcurrencyType)
viewContext.persistentStoreCoordinator = coordinator
viewContext.mergePolicy = NSMergePolicy.rollback
token = NotificationCenter
.default
.addObserver(forName: NSNotification.Name.NSManagedObjectContextDidSave,
object: nil,
queue: nil) { (notification) in
guard let context = notification.object as? NSManagedObjectContext else {
return
}
guard context != self.viewContext &&
context.persistentStoreCoordinator == self.viewContext.persistentStoreCoordinator else {
return
}
self.viewContext.performAndWait {
if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
updatedObjects.forEach {
self.viewContext
.object(with: $0.objectID)
.willAccessValue(forKey: nil)
}
}
self.viewContext.mergeChanges(fromContextDidSave: notification)
}
}
}
deinit {
if let token = token {
NotificationCenter.default.removeObserver(token)
}
}
private static func model(for name: String) -> NSManagedObjectModel? {
guard let url = Bundle.main.url(forResource: name, withExtension: "momd") else {
return nil
}
return NSManagedObjectModel(contentsOf: url)
}
}
extension CoreDataManager {
func save(_ context: NSManagedObjectContext) throws {
guard context != viewContext else {
fatalError("Saving the viewContext is illegal.")
}
if context.hasChanges {
try context.save()
}
}
func perform(_ block: @escaping CoreDataManagerUpdateBlock) {
perform(block, completion: nil)
}
func perform(_ block: @escaping CoreDataManagerUpdateBlock, completion: (() -> ())?) {
let context = viewContext.backgroundCloneRW
var shouldSave = true
queue.async {
context.performAndWait {
block(context, &shouldSave)
}
if shouldSave {
try? context.save()
}
context.killed = true
completion?()
}
}
func query(_ block: @escaping CoreDataManagerQueryBlock) {
let context = viewContext.backgroundCloneRO
queue.async {
context.performAndWait {
block(context)
context.killed = true
}
}
}
func viewQuery(_ block: CoreDataManagerQueryBlock) {
viewContext.performAndWait {
block(viewContext)
}
if viewContext.hasChanges {
fatalError("viewContext should not be modified.")
}
}
}
//MARK: - Public - NSManagedObject -
extension NSManagedObject {
static var entityName: String {
return self.entity().name ?? String(describing: type(of: self))
}
}
//MARK: - Public - NSManagedObjectContext -
extension NSManagedObjectContext {
func itemsMatching<T : NSManagedObject>(conditions: [String: Any],
for entity: T.Type) throws -> [T] {
let request = NSFetchRequest<NSManagedObject>(entityName: T.entityName)
request.predicate = NSPredicate.predicate(for: conditions)
return try self.fetch(request) as! [T]
}
func load<T : NSManagedObject>(items: [T]) throws -> [T] {
let request = NSFetchRequest<NSManagedObject>(entityName: T.entityName)
request.predicate = NSPredicate(format: "SELF IN %@", argumentArray: [items])
return try self.fetch(request) as! [T]
}
func load<T : NSManagedObject>(item: T) throws -> T {
return try existingObject(with: item.objectID) as! T
}
}
//MARK: - Private
private extension NSManagedObjectContext {
var backgroundCloneRW: KillableMOC {
let context = KillableMOC(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = self.persistentStoreCoordinator
context.mergePolicy = NSMergePolicy.overwrite
return context
}
var backgroundCloneRO: ReadOnlyMOC {
let context = ReadOnlyMOC(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = self.persistentStoreCoordinator
context.mergePolicy = NSMergePolicy.rollback
return context
}
}
//MARK: - Semi-private classes
class KillableMOC: NSManagedObjectContext {
fileprivate var killed = false
override func performAndWait(_ block: () -> Void) {
guard killed == false else {
fatalError("Dead context reused.")
}
super.performAndWait(block)
}
override func perform(_ block: @escaping () -> Void) {
guard killed == false else {
fatalError("Dead context reused.")
}
super.perform(block)
}
override func fetch(_ request: NSFetchRequest<NSFetchRequestResult>) throws -> [Any] {
guard killed == false else {
fatalError("Dead context reused.")
}
return try super.fetch(request)
}
}
class ReadOnlyMOC: KillableMOC {
override func save() throws {
fatalError("Can't save a read-only context")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment