forked from chriseidhof/NSIncrementalStore-Test-Project
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added NSIncrementalStore+Async helpers
- Loading branch information
1 parent
ec1698b
commit 607fe42
Showing
2 changed files
with
294 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// | ||
// NSIncrementalStore+Async.h | ||
// | ||
// Created by Martijn Thé on 3/1/12. | ||
// Copyright (c) 2012 martijnthe.nl All rights reserved. | ||
// | ||
|
||
/** | ||
* NSIncrementalStore+Async | ||
* | ||
* The goal of this set of helpers is to make it easy to write an NSIncrementalStore subclasses that | ||
* have slow backing stores, e.g. web services or remote databases. | ||
* | ||
* It helps you by abstracting away the details of executing NSFetchRequests in the background, faulting | ||
* the results back into the caller's context, etc. | ||
* | ||
* NSFetchRequest (and later NSAsyncSaveChangesRequest) are subclassed and decorated with the | ||
* protocol NSAsyncIncrementalStoreRequest. This protocol provides additional properties and methods like | ||
* willStartBlock, didFinishBlock, -cancel, queue, etc. | ||
* These 'async' requests return immediately when executed, either returning cached objects that match the request | ||
* or an empty array, depending on the returnsCachedResultsImmediately property of the request and the store's cache itself. | ||
* | ||
* | ||
* How-to | ||
* | ||
* In your NSIncrementalStore subclass, do all blocking work inside -executeRequestBlocking:withContext:error: | ||
* (or rename your current -executeRequest:withContext:error: if you already had one). | ||
* Avoid doing blocking work inside -newValuesForObjectWithID:withContext:error:, it is assumed you have already loaded | ||
* the data in -executeRequestBlocking:.. and have cached the values. | ||
* Call -executeRequestAsyncAndSync:withContext:error: inside the required -executeRequest:withContext:error: method. | ||
* Just pass the arguments and return the result directly. | ||
* | ||
* Then use NSAsyncFetchRequest whereever you want to fetch objects asynchronically. | ||
* Optionally set it's additional properties like didFinishBlock, returnsCachedResultsImmediately, etc. | ||
* Optionally implement the NSIncrementalStoreExecuteRequestCached and NSIncrementalStoreRequestQueuing protocols on the store. | ||
* | ||
* | ||
* Assumptions about your incremental store subclass | ||
* | ||
* - All blocking work is done inside -executeRequestBlocking:withContext:error: | ||
* - Other required methods like, -newValuesForObjectWithID:withContext:error:, should not be blocking | ||
* - Your incremental store needs to be able to handle fetch requests with resultType NSManagedObjectIDResultType. | ||
* | ||
* | ||
* Todos: | ||
* | ||
* - Handle save requests | ||
* - Add documentation to the methods, properties, etc. | ||
* - Respect resultType of request when calling the didFinishBlock | ||
* - Add a convenience method to dynamically replace the -executeRequest: with -executeRequestAsyncAndSync: and add -executeRequestBlocking: | ||
* - Optimize the performance of the process of faulting objects into the caller's context | ||
* - Add a block handler to report the progress? | ||
* | ||
**/ | ||
|
||
|
||
#import <CoreData/CoreData.h> | ||
|
||
#pragma mark - General stuff, errors, etc. | ||
|
||
extern NSString* NSAsyncIncrementalStoreErrorDomain; | ||
|
||
typedef enum { | ||
NSAsyncIncrementalStoreErrorUnimplemented, | ||
NSAsyncIncrementalStoreErrorCancelled, | ||
NSAsyncIncrementalStoreErrorExecutingRequest | ||
} NSAsyncIncrementalStoreError; | ||
|
||
|
||
|
||
#pragma mark - NSPersistentStoreRequest helpers, protocols, templates, etc. | ||
|
||
@protocol NSAsyncIncrementalStoreRequest <NSObject> | ||
@required // perhaps make some @optional later on | ||
@property (nonatomic, readwrite, strong) void(^willStartBlock)(void); | ||
@property (nonatomic, readwrite, strong) void(^didFinishBlock)(id results, NSError *error); | ||
@property (nonatomic, readwrite, assign) BOOL returnsCachedResultsImmediately; // should default to YES | ||
@property (nonatomic, readwrite, strong) NSOperationQueue* queue; // if nil, the -queueForRequest: will be called on the store | ||
@property (atomic, readonly, assign) BOOL isCancelled; | ||
- (void)cancel; | ||
@end | ||
|
||
@interface NSAsyncFetchRequest : NSFetchRequest <NSAsyncIncrementalStoreRequest> @end | ||
//@interface NSAsyncSaveChangesRequest : NSSaveChangesRequest <NSAsyncIncrementalStoreRequest> @end // TODO | ||
|
||
|
||
|
||
#pragma mark - NSIncrementalStore helpers, protocols, templates, etc. | ||
|
||
@interface NSIncrementalStore (Async) | ||
- (id)executeRequestAsyncAndSync:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)context error:(NSError**)error; | ||
@end | ||
|
||
@protocol NSIncrementalStoreExecuteRequestBlocking <NSObject> | ||
@required | ||
- (id)executeRequestBlocking:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)context error:(NSError**)error; | ||
@end | ||
|
||
@protocol NSIncrementalStoreExecuteRequestCached <NSObject> | ||
@required | ||
- (id)executeRequestCached:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)context error:(NSError**)error; | ||
@end | ||
|
||
@protocol NSIncrementalStoreRequestQueuing <NSObject> | ||
- (NSOperationQueue*)queueForRequest:(NSPersistentStoreRequest*)request; // if nil or not implemented, a new queue will be used for each request | ||
@end |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
// | ||
// NSIncrementalStore+Async.m | ||
// | ||
// Created by Martijn Thé on 3/1/12. | ||
// Copyright (c) 2012 martijnthe.nl All rights reserved. | ||
// | ||
|
||
#import "NSIncrementalStore+Async.h" | ||
|
||
NSString* NSAsyncIncrementalStoreErrorDomain = @"nl.martijnthe.asyncincrementalstore"; | ||
|
||
@implementation NSAsyncFetchRequest | ||
@synthesize willStartBlock; | ||
@synthesize didFinishBlock; | ||
@synthesize returnsCachedResultsImmediately; | ||
@synthesize queue; | ||
@synthesize isCancelled; | ||
|
||
- (void)cancel { | ||
@synchronized(self) { | ||
isCancelled = YES; | ||
} | ||
} | ||
|
||
@end | ||
|
||
|
||
#pragma mark - NSIncrementalStore helpers, protocols, templates, etc. | ||
|
||
@implementation NSIncrementalStore (Async) | ||
|
||
#define callDidFinishBlock(callerQueue, asyncRequest, results, error) \ | ||
if (asyncRequest.didFinishBlock) { \ | ||
NSOperationQueue* _callerQueue = callerQueue; \ | ||
if (_callerQueue) { \ | ||
[_callerQueue addOperationWithBlock:^{ \ | ||
asyncRequest.didFinishBlock(results, error); \ | ||
asyncRequest.didFinishBlock = nil; \ | ||
}]; \ | ||
} else { \ | ||
asyncRequest.didFinishBlock(results, error); \ | ||
asyncRequest.didFinishBlock = nil; \ | ||
} \ | ||
} | ||
|
||
- (id)executeRequestAsyncAndSync:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)callerMoc error:(NSError**)error { | ||
if ([request conformsToProtocol:@protocol(NSAsyncIncrementalStoreRequest)] == NO) { | ||
// request is not an async request, just go down the regular path: | ||
return [(id<NSIncrementalStoreExecuteRequestBlocking>)self executeRequestBlocking:request withContext:callerMoc error:error]; | ||
} else { | ||
NSPersistentStoreRequest<NSAsyncIncrementalStoreRequest>* asyncRequest = (NSPersistentStoreRequest<NSAsyncIncrementalStoreRequest>*)request; | ||
|
||
// Keep a reference to the original queue, we want to run the handler ('delegate') blocks on this queue. | ||
NSOperationQueue* callerQueue = [NSOperationQueue currentQueue]; | ||
|
||
if (asyncRequest.isCancelled) { | ||
NSError* cancelError = [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorCancelled userInfo:nil]; | ||
if (error) { | ||
*error = cancelError; | ||
} | ||
callDidFinishBlock(nil, asyncRequest, nil, cancelError); | ||
return nil; | ||
} | ||
|
||
// We're dealing with an async request, | ||
// so 2 things need to be done: 1) execute the request on a queue, 2) return cached results immediately (optionally) | ||
|
||
// 1) | ||
// Figure out which queue to use by cascading: | ||
NSOperationQueue* requestQueue; | ||
if (asyncRequest.queue) { | ||
requestQueue = asyncRequest.queue; | ||
} | ||
if (requestQueue == nil && [self conformsToProtocol:@protocol(NSIncrementalStoreRequestQueuing)]) { | ||
requestQueue = [(id<NSIncrementalStoreRequestQueuing>)self queueForRequest:asyncRequest]; | ||
} | ||
if (requestQueue == nil) { | ||
requestQueue = [[NSOperationQueue alloc] init]; | ||
} | ||
|
||
[requestQueue addOperationWithBlock:^{ | ||
|
||
// Check if cancelled: | ||
if (asyncRequest.isCancelled) { | ||
callDidFinishBlock(callerQueue, asyncRequest, nil, [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorCancelled userInfo:nil]); | ||
return; | ||
} | ||
|
||
if (asyncRequest.willStartBlock) { | ||
[callerQueue addOperationWithBlock:^{ | ||
asyncRequest.willStartBlock(); | ||
asyncRequest.willStartBlock = nil; | ||
}]; | ||
} | ||
|
||
// Prepare background request + moc: | ||
NSManagedObjectContext* bgMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType]; | ||
// bgMoc.parentContext = self.moc; // <-- this will cause a call executeRequest:withContext:error: to the store on the CALLER's (MAIN) THREAD... :-S | ||
bgMoc.persistentStoreCoordinator = callerMoc.persistentStoreCoordinator; | ||
NSPersistentStoreRequest<NSAsyncIncrementalStoreRequest>* bgRequest; | ||
switch (asyncRequest.requestType) { | ||
case NSFetchRequestType: { | ||
NSFetchRequest<NSAsyncIncrementalStoreRequest>* bgFetchRequest = [asyncRequest copy]; | ||
bgFetchRequest.entity = [NSEntityDescription entityForName:bgFetchRequest.entityName inManagedObjectContext:bgMoc]; | ||
bgFetchRequest.resultType = NSManagedObjectIDResultType; // get objectIDs, we fault the objects into the callerMoc later on | ||
bgRequest = bgFetchRequest; | ||
break; | ||
} | ||
case NSSaveRequestType: { | ||
|
||
// TODO: | ||
// NSSaveChangesRequests contain references to the inserted, updated and deleted objects that need to be saved. | ||
// We cannot use these objects on this queue (thread/queue confinement). | ||
// Faulting them into this context is not going to work either, because then they won't contain the changes. | ||
// So at some point in time, the changed objects faulted into the bg context and | ||
// their changed properties need to be copied from the objects in the caller moc. | ||
|
||
// break; // for now, fall-thru | ||
} | ||
default: { | ||
// Finish with 'unimplemented' error: | ||
callDidFinishBlock(callerQueue, asyncRequest, nil, [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorUnimplemented userInfo:nil]); | ||
return; | ||
} | ||
} | ||
|
||
// Finally, run the (blocking) request: | ||
NSError* executeError = nil; | ||
NSArray* objectIDs = [(id<NSIncrementalStoreExecuteRequestBlocking>)self executeRequestBlocking:bgRequest withContext:callerMoc error:error]; | ||
|
||
// Handle executeError: | ||
if (objectIDs == nil) { | ||
NSDictionary* userInfo = nil; | ||
if (error) { | ||
userInfo = [NSDictionary dictionaryWithObjectsAndKeys:executeError, NSUnderlyingErrorKey, nil]; | ||
} | ||
NSError* wrappedError = [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorExecutingRequest userInfo:userInfo]; | ||
callDidFinishBlock(callerQueue, asyncRequest, nil, wrappedError); | ||
return; | ||
} | ||
|
||
// Check if cancelled: | ||
if (asyncRequest.isCancelled) { | ||
callDidFinishBlock(callerQueue, asyncRequest, nil, [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorCancelled userInfo:nil]); | ||
return; | ||
} | ||
|
||
// Fault objects into callerMoc & refresh objects: | ||
[callerQueue addOperationWithBlock:^{ | ||
// At this point in time, the newly fetched objects are not registered in callerMoc, but only in bgMoc | ||
|
||
NSError* lookupError = nil; | ||
NSManagedObject* mObject; | ||
NSMutableArray* results = [NSMutableArray arrayWithCapacity:[objectIDs count]]; | ||
for (NSManagedObjectID *objectID in objectIDs) { | ||
// -existingObjectWithID:error: causes the object to "register" in the callerMoc (-managedObjectContextDidRegisterObjectsWithIDs: is also called on the IncrementalStore). | ||
// It also fires the fault, which is important, because the objects like NSFetchedResultsController will filter the registeredObjects in-memory based on its the fetchRequest. | ||
mObject = [callerMoc existingObjectWithID:objectID error:&lookupError]; | ||
// Interestingly, even though existingObjectWithID:error: will cause objects to be "registered" in the context, its does not cause NSManagedObjectContextObjectsDidChangeNotifications to fire... | ||
if (mObject) { | ||
// -refreshObject:mergeChanges: trigger the NSManagedObjectContextObjectsDidChangeNotification which drives the change-monitoring features of objects like NSFetchedResultsController | ||
// NOTE: I suspect this could be optimized to avoid triggering change notifications for objects that were already in the callerMoc AND have not been changed in the mean while. | ||
[callerMoc refreshObject:mObject mergeChanges:YES]; | ||
[results addObject:mObject]; | ||
} else { | ||
NSLog(@"Error trying to look up object in moc: %@", lookupError); | ||
} | ||
} | ||
|
||
// TODO: respect the resultType of a fetch request (now we're always returning managed objects) | ||
|
||
// Done without errors: | ||
callDidFinishBlock(nil, asyncRequest, results, nil); | ||
}]; | ||
}]; | ||
|
||
// 2) | ||
// If cache is requested immediately and the store implements the method to do that, return cache: | ||
if (asyncRequest.returnsCachedResultsImmediately && [self conformsToProtocol:@protocol(NSIncrementalStoreExecuteRequestCached)]) { | ||
return [(id<NSIncrementalStoreExecuteRequestCached>)self executeRequestCached:request withContext:callerMoc error:error]; | ||
} else { | ||
// Else, always return an empty array, otherwise an error will be assumed by the caller: | ||
return [NSArray array]; | ||
} | ||
} | ||
} | ||
|
||
@end |