Skip to content

Instantly share code, notes, and snippets.

@indragiek
Last active March 5, 2023 21:55
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save indragiek/5297435 to your computer and use it in GitHub Desktop.
Save indragiek/5297435 to your computer and use it in GitHub Desktop.
Draft of a ReactiveCocoa based interface for CoreData
//
// FGOManagedObjectContextStack.h
//
// Created by Indragie Karunaratne on 2012-12-23.
//
#import <Foundation/Foundation.h>
typedef void (^FGOConfigurationBlock)(id);
@interface FGOManagedObjectContextStack : NSObject
@property (nonatomic, strong, readonly) NSManagedObjectContext *backgroundContext;
@property (nonatomic, strong, readonly) NSManagedObjectContext *mainQueueContext;
@property (nonatomic, strong, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (nonatomic, strong, readonly) NSManagedObjectModel *managedObjectModel;
- (id)initWithModelName:(NSString *)modelName;
- (RACSignal *)saveChanges;
#pragma mark - Fetching
- (RACSignal *)fetchWithRequest:(NSFetchRequest *)request;
- (RACSignal *)fetchExistingObjectWithRequest:(NSFetchRequest *)request;
#pragma mark - Insertion
- (RACSignal *)createObjectOfEntityName:(NSString *)entityName configure:(FGOConfigurationBlock)block;
- (RACSignal *)fetchOrCreateObjectWithRequest:(NSFetchRequest *)request configure:(FGOConfigurationBlock)block;
#pragma mark - Concurrency Helpers
// Should only be called from inside -performBlock: of the background MOC
- (id)backgroundObjectWithID:(NSManagedObjectID *)objectID;
// Should only be called from the main thread
- (id)mainQueueObjectWithID:(NSManagedObjectID *)objectID;
- (NSArray *)mainQueueObjectsWithIDs:(NSArray *)objectIDs;
@end
//
// FGOManagedObjectContextStack.m
//
// Created by Indragie Karunaratne on 2012-12-23.
//
#import "FGOManagedObjectContextStack.h"
#import "NSURL+FGOApplicationDirectories.h"
@implementation FGOManagedObjectContextStack
- (id)initWithModelName:(NSString *)modelName
{
if ((self = [super init])) {
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:modelName withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSURL *dataURL = [NSURL fgo_applicationSupportDirectory];
NSURL *storeURL = [dataURL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.fgodata", modelName]];
NSError *coreDataError = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&coreDataError]) {
FGOGenericErrorLog(@"Error adding persistent store", coreDataError);
return nil;
}
_mainQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainQueueContext performBlockAndWait:^{
_mainQueueContext.persistentStoreCoordinator = _persistentStoreCoordinator;
_mainQueueContext.undoManager = nil;
}];
_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundContext performBlockAndWait:^{
_backgroundContext.parentContext = _mainQueueContext;
_backgroundContext.undoManager = nil;
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification object:[NSApplication sharedApplication] queue:nil usingBlock:^(NSNotification *note) {
[self.backgroundContext performBlockAndWait:^{
[self.backgroundContext fgo_saveChanges];
}];
[self.mainQueueContext performBlockAndWait:^{
[self.mainQueueContext fgo_saveChanges];
}];
}];
}
return self;
}
#pragma mark - Public Methods
- (RACSignal *)saveChanges
{
return [RACSignal startWithScheduler:[RACScheduler scheduler] subjectBlock:^(RACSubject *subject) {
__block NSError *error = nil;
[self.backgroundContext performBlock:^{
[self.backgroundContext save:&error];
[self.mainQueueContext performBlock:^{
[self.mainQueueContext save:&error];
if (error) [subject sendError:error];
[subject sendCompleted];
}];
}];
}];
}
- (RACSignal *)fetchWithRequest:(NSFetchRequest *)request
{
[request setResultType:NSManagedObjectIDResultType];
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self.backgroundContext performBlock:^{
NSError *error = nil;
NSArray *results = [self.backgroundContext executeFetchRequest:request error:&error];
if (error) [subscriber sendError:error];
else {
[self.mainQueueContext performBlock:^{
NSArray *objects = [self mainQueueObjectsWithIDs:results];
[subscriber sendNext:objects];
[subscriber sendCompleted];
}];
}
}];
return nil;
}];
}
- (RACSignal *)createObjectOfEntityName:(NSString *)entityName configure:(FGOConfigurationBlock)block
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self.backgroundContext performBlock:^{
id object = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:self.backgroundContext];
if (block) block(object);
__block NSError *error = nil;
[self.backgroundContext save:&error];
[self.mainQueueContext performBlockAndWait:^{
[self.mainQueueContext save:&error];
}];
[self.backgroundContext refreshObject:object mergeChanges:NO];
NSManagedObjectID *objectID = [object fgo_permanentObjectID];
[self.mainQueueContext performBlock:^{
NSError *existingError = nil;
NSManagedObject *managedObject = [self.mainQueueContext existingObjectWithID:objectID
error:&existingError];
if (existingError)
FGOGenericErrorLog(@"Error attempting to fetch object using the object ID. Trying a fetch request.", existingError);
if (!managedObject) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
request.predicate = [NSPredicate predicateWithFormat:@"SELF == %@", object];
request.fetchLimit = 1;
NSArray *results = [self.mainQueueContext executeFetchRequest:request error:&error];
managedObject = [results count] ? results[0] : nil;
}
if (error) [subscriber sendError:error];
[subscriber sendNext:managedObject];
[subscriber sendCompleted];
}];
}];
return nil;
}];
}
- (RACSignal *)fetchOrCreateObjectWithRequest:(NSFetchRequest *)request configure:(FGOConfigurationBlock)block
{
return [[self fetchWithRequest:request] flattenMap:^RACStream *(NSArray *results) {
if (results.count) {
return [RACSignal return:results[0]];
} else {
return [self createObjectOfEntityName:request.entityName configure:block];
}
}];
}
- (RACSignal *)fetchExistingObjectWithRequest:(NSFetchRequest *)request
{
request.fetchLimit = 1;
return [[self fetchWithRequest:request] flattenMap:^RACStream *(NSArray *results) {
if (results.count) {
return [RACSignal return:results[0]];
}
return [RACSignal empty];
}];
}
- (id)backgroundObjectWithID:(NSManagedObjectID *)objectID
{
if (!objectID) return nil;
NSError *error = nil;
NSManagedObject *obj = [self.backgroundContext existingObjectWithID:objectID error:&error];
if (error) FGOGenericErrorLog(@"Error fetching object", error);
return obj;
}
- (id)mainQueueObjectWithID:(NSManagedObjectID *)objectID
{
if (!objectID) return nil;
NSError *error = nil;
NSManagedObject *obj = [self.mainQueueContext existingObjectWithID:objectID error:&error];
if (error) FGOGenericErrorLog(@"Error fetching object", error);
return obj;
}
- (NSArray *)mainQueueObjectsWithIDs:(NSArray *)objectIDs
{
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[objectIDs count]];
[self.mainQueueContext performBlockAndWait:^{
[objectIDs enumerateObjectsUsingBlock:^(NSManagedObjectID *objectID, NSUInteger idx, BOOL *stop) {
NSError *error = nil;
NSManagedObject *existingObject = [self.mainQueueContext existingObjectWithID:objectID error:&error];
if (error)
FGOGenericErrorLog([NSString stringWithFormat:@"Failed to fetch object with ID %@", objectID], error);
if (existingObject)
[objects addObject:existingObject];
}];
}];
return objects;
}
@end
@shpakovski
Copy link

Great code, thanks for sharing. Does it make sense to create one more “root” managed object context of NSPrivateQueueConcurrencyType that would be used only for saving data in background? More code, but no UI freeze while the database is being saved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment