Skip to content

Instantly share code, notes, and snippets.

@kreeger
Last active August 29, 2015 13:56
Show Gist options
  • Select an option

  • Save kreeger/8920226 to your computer and use it in GitHub Desktop.

Select an option

Save kreeger/8920226 to your computer and use it in GitHub Desktop.
Core Data context management.

BDKCoreDataStore & BDKCoreDataOperation

These two classes are designed to work together to provide access to a common Core Data store, along with the contexts that go with it. The main NSManagedObjectContext is designed to be accessed on the main thread only. Safe background operations happen in a background NSOperationQueue to ensure that multiple reads and writes don't step on each others' toes.

It could use a tad of cleanup, but it's served me well for a few apps.

@import Foundation;
@import CoreData;
/**
Handles all Core Data management.
*/
@interface BDKCoreDataStore : NSObject
/**
A common Core Data operation queue where all writes go.
*/
@property (readonly, nonatomic) NSOperationQueue *coreDataQueue;
/**
The singleton instance.
*/
+ (instancetype)sharedInstance;
/**
A shortcut method to return the singleton instance's `mainMOC` property.
@return The main managed object context, for use in the main thread only.
*/
+ (NSManagedObjectContext *)mainThreadContext;
/**
Initializes and returns a version of this store object with a name that matches the Core Data model
you're using.
@param name The name of your Core Data managed object model.
@return An instance of this store; you may want to keep it in your AppDelegate.
*/
+ (instancetype)storeWithName:(NSString *)name;
/**
Saves the managed object context.
*/
- (void)save;
/**
Generates and returns a fresh private queue managed object context.
@return A private NSManagedObjectContext.
*/
- (NSManagedObjectContext *)freshPrivateContext;
/**
Returns the URL to the application's Documents directory.
@return A URL to the app's Documents directory.
*/
- (NSURL *)applicationDocumentsDirectory;
/**
Handles removing observer methods from a private context.
@param context The context from which to remove observers.
*/
- (void)cleanupPrivateContext:(NSManagedObjectContext *)context;
@end
#import "BDKCoreDataStore.h"
@interface BDKCoreDataStore ()
/**
The managed object context for the application.
*/
@property (strong, nonatomic) NSManagedObjectContext *mainMOC;
/**
Stores a reference to the name of the Core Data Model.
*/
@property (strong, nonatomic) NSString *storeName;
/**
The managed object model for the application.
*/
@property (strong, nonatomic) NSManagedObjectModel *mom;
/**
The persistent store coordinator for the application.
*/
@property (strong, nonatomic) NSPersistentStoreCoordinator *psc;
/**
A common Core Data operation queue where all writes go.
*/
@property (strong, nonatomic) NSOperationQueue *coreDataQueue;
/**
Initializes and returns a version of this store object with a name that matches the Core Data model
you're using.
@param name The name of your Core Data managed object model.
@return An instance of this store; you may want to keep it in your AppDelegate.
*/
- (instancetype)initWithName:(NSString *)name;
/**
Registers this store object with the NSManagedObjectContextDidSaveNotification.
*/
- (void)setupSaveNotification;
@end
@implementation BDKCoreDataStore
+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [self storeWithName:@"NameOfMyStore"];
});
return _sharedInstance;
}
+ (NSManagedObjectContext *)mainThreadContext {
return [[self sharedInstance] mainMOC];
}
+ (instancetype)storeWithName:(NSString *)name {
return [[self alloc] initWithName:name];
}
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (!self) return nil;
_storeName = name;
_coreDataQueue = [NSOperationQueue new];
[_coreDataQueue setName:@"gr.kree.BDKCoreDataStoreOperationQueue"];
[_coreDataQueue setMaxConcurrentOperationCount:1];
_mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainMOC setPersistentStoreCoordinator:self.psc];
[_mainMOC setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[self setupSaveNotification];
return self;
}
#pragma mark - Private properties
- (NSManagedObjectModel *)mom {
if (_mom) return _mom;
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:self.storeName withExtension:@"momd"];
_mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _mom;
}
- (NSPersistentStoreCoordinator *)psc {
if (_psc) return _psc;
_psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.mom];
NSString *storeFileName = [self.storeName stringByAppendingPathExtension:@"sqlite3"];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:storeFileName];
NSError *error = nil;
NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption: @YES,
NSInferMappingModelAutomaticallyOption: @YES};
if (![_psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options
error:&error]) {
// Try blasting the store out, if that doesn't work, fail hard
NSError *innerError = nil;
NSLog(@"Deleting store at URL %@.", storeURL);
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:&innerError];
if (innerError || ![_psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options
error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
return _psc;
}
#pragma mark - Public methods
- (void)save {
NSError *error = nil;
NSManagedObjectContext *moc = self.mainMOC;
if (!moc) return;
if ([moc hasChanges]) {
BOOL success = [moc save:&error];
if (error) {
NSLog(@"Core data error %@, %@.", error, [error userInfo]);
abort();
}
if (!success) {
NSLog(@"Core data did not save properly.");
} else {
NSLog(@"Just saved NSManagedObjectContext for main thread.");
}
} else {
NSLog(@"Skipped saving NSManagedObjectContext because it didn't have any changes.");
}
}
#pragma mark - Private methods
- (void)setupSaveNotification {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:self.mainMOC];
}
- (void)contextDidSave:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
NSManagedObjectContext *moc = self.mainMOC;
if (![[notification object] isEqual:moc]) {
[moc performBlock:^{
NSLog(@"Merging changes from private context %p.", [notification object]);
[moc mergeChangesFromContextDidSaveNotification:notification];
}];
}
});
}
- (NSManagedObjectContext *)freshPrivateContext {
NSManagedObjectContext *context = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setPersistentStoreCoordinator:self.psc];
[context setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
// This is default on iOS, but hey
[context setUndoManager:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:context];
return context;
}
- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
}
- (void)cleanupPrivateContext:(NSManagedObjectContext *)context {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSManagedObjectContextDidSaveNotification
object:context];
context = nil;
}
@end
@import Foundation;
@import CoreData;
@class BDKCoreDataStore;
/**
Designed to work with BDKCoreDataStore, this easy-to-use operation fires off a background process for
context saving (and does so with a fresh context).
*/
@interface BDKCoreDataStore : NSOperation
/**
Stores the block to execute in the background.
*/
@property (nonatomic, copy) void (^backgroundOperation)(NSManagedObjectContext *innerContext);
/**
Stores a block to be executed when complete; will be passed an error, if one occurred during a
Core Data save. Otherwise, success will be YES.
*/
@property (nonatomic, copy) void (^completion)(BOOL success, NSError *error);
/**
The instance of BDKCoreDataStore that this operation is using.
*/
@property (readonly) BDKCoreDataStore *coreDataStore;
/**
Returns a Boolean value indicating whether the operation is currently executing.
*/
@property (atomic) BOOL isExecuting;
/**
Returns a Boolean value indicating whether the operation is done executing.
*/
@property (atomic) BOOL isFinished;
/**
Creates an instance of this operation with an operation block (which is passed a fresh context),
and immediately inserts it into a queue.
@param coreDataStore The instance of a BDKCoreDataStore to work with.
@param operation The block in which to execute background code.
@param completion A block to be called upon completion; will be passed an error, if one occurred.
*/
+ (void)performInBackgroundWithCoreDataStore:(BDKCoreDataStore *)coreDataStore
backgroundOperation:(void (^)(NSManagedObjectContext *innerContext))operation
completion:(void (^)(BOOL success, NSError *error))completion;
/**
Creates an instance of this operation with an operation block (which is passed a fresh context).
@param coreDataStore The instance of a BDKCoreDataStore to work with.
@param operation The block in which to execute background code.
@param completion A block to be called upon completion; will be passed an error, if one occurrend.
@return An instance of this operation.
*/
- (instancetype)initWithCoreDataStore:(BDKCoreDataStore *)coreDataStore
backgroundOperation:(void (^)(NSManagedObjectContext *innerContext))operation
completion:(void (^)(BOOL success, NSError *error))completion;
@end
#import "BDKCoreDataOperation.h"
#import "BDKCoreDataStore.h"
@implementation BDKCoreDataOperation
@synthesize coreDataStore = _coreDataStore;
+ (void)performInBackgroundWithCoreDataStore:(BDKCoreDataStore *)coreDataStore
backgroundOperation:(void (^)(NSManagedObjectContext *))operation
completion:(void (^)(BOOL, NSError *))completion {
BDKCoreDataOperation *op = [[self alloc] initWithCoreDataStore:coreDataStore
backgroundOperation:operation
completion:completion];
[coreDataStore.coreDataQueue addOperation:op];
}
- (instancetype)initWithCoreDataStore:(BDKCoreDataStore *)coreDataStore
backgroundOperation:(void (^)(NSManagedObjectContext *))operation
completion:(void (^)(BOOL, NSError *))completion {
self = [super init];
if (!self) return nil;
_coreDataStore = coreDataStore;
_backgroundOperation = operation;
_completion = completion;
return self;
}
- (BOOL)isConcurrent {
return NO;
}
- (void)main {
NSManagedObjectContext *innerContext = [self.coreDataStore freshPrivateContext];
self.backgroundOperation(innerContext);
NSError *error = nil;
if ([innerContext hasChanges]) {
NSLog(@"Saving private context %p with %i inserts, %i updates, %i deletes.",
innerContext,
[[innerContext insertedObjects] count],
[[innerContext updatedObjects] count],
[[innerContext deletedObjects] count]);
[innerContext save:&error];
}
[self.coreDataStore cleanupPrivateContext:innerContext];
innerContext = nil;
if (self.completion) {
self.completion(!!error, error);
}
}
- (void)start {
self.isExecuting = YES;
self.isFinished = NO;
[self main];
self.isExecuting = NO;
self.isFinished = YES;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment