Created
October 24, 2017 08:47
-
-
Save ashevin/c12daa7ce1ecefc846f3d0ddff64d853 to your computer and use it in GitHub Desktop.
CoreDataManager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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