Last active
March 5, 2023 21:55
-
-
Save indragiek/5297435 to your computer and use it in GitHub Desktop.
Draft of a ReactiveCocoa based interface for CoreData
This file contains hidden or 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
// | |
// 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 |
This file contains hidden or 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
// | |
// 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.