Core Data and multithreading (and Bindings to make it more fun) - multithreading

I have got this background thread that does a few things with core data objects. I get the context as follows:
- (id)_managedObjectContextForThread;
{
NSManagedObjectContext * newContext = [[[NSThread currentThread] threadDictionary] valueForKey:#"managedObjectContext"];
if(newContext) return newContext;
newContext = [[NSManagedObjectContext alloc] init];
[newContext setPersistentStoreCoordinator:[[[NSApplication sharedApplication] delegate] persistentStoreCoordinator]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(_mergeChangesFromManagedObjectContext:)
name:NSManagedObjectContextDidSaveNotification
object:newContext];
[[[NSThread currentThread] threadDictionary] setValue:newContext forKey:#"managedObjectContext"];
return newContext;
}
then I fetch some objects, modify them and save the context:
- (void) saveContext:(NSManagedObjectContext*)context {
NSError *error = nil;
if (![context save:&error]) {
[[NSApplication sharedApplication] presentError:error];
}
}
- (void)_mergeChangesFromManagedObjectContext:(NSNotification*)notification;
{
[[[[NSApplication sharedApplication] delegate] managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
.. later I remove the observer. This works for the main part. But some properties don't get updated when they get merged back. The properties that were nil before get updated. The ones that had a value stay the same.
I tried:
[newContext setMergePolicy:NSOverwriteMergePolicy];
... (and the other merge policies) on the main context but it did not work :P
Thank you for your help.
Note: I have bound the values to a NSTableView. I log them after the merge. The values properties that were nil seem to work fine.

How are you registering both contexts for notifications? You need to do something like this:
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundMOC];
[nc addObserver:self
selector:#selector(mainContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:mainMOC];
And implement the callbacks:
// merge changes in background thread if main context changes
- (void)mainContextDidSave:(NSNotification *)notification
{
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[backgroundMOC performSelector:selector onThread:background_thread withObject:notification waitUntilDone:NO];
}
// merge changes in main thread if background context changes
- (void)backgroundContextDidSave:(NSNotification *)notification
{
if ([NSThread isMainThread]) {
[mainMOC mergeChangesFromContextDidSaveNotification:notification];
}
else {
[self performSelectorOnMainThread:#selector(backgroundContextDidSave:) withObject:notification waitUntilDone:NO];
}
}

Related

Core Data Parent/Child context save fail

I setup a background thread with the Parent/Child model. Essentially the context save is failing.
Here is my setup. In the AppDelegate i've setup the _managedObjectContext with the NSMainQueueConcurrencyType:
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];//[[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In my data loading class I setup the parent/child mocs here to perform the work on the background thread:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSManagedObjectContext *mainMOC = self.managedObjectContext;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[moc setParentContext:mainMOC];
[moc setUndoManager:nil];
When the json data has completed I attempt to peform a save operation with the following macro:
#define SAVE_MOC { NSError *error; \
if (![moc save:&error]) { NSLog(#"Sub MOC Error"); } \
[mainMOC performBlock:^{ NSError *e = nil; if (![mainMOC save:&e]) {
NSLog(#"Main MOC Error %#",error.localizedDescription);}}];}
Also when i've completed the data load I jump back on the main thread like this:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"<---- complete CS sub moc! ---->");
//this fires ok
});
So, from my SAVE_MOC macro i just get a simple error:
Main MOC Error (null)
Let me know if I can provide more info. I'm very new to multi-threading and trying to get a better handle on this approach.
Thanks,
Josh
In my data loading class I setup the parent/child mocs here to perform
the work on the background thread:
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSManagedObjectContext *mainMOC = self.managedObjectContext;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
You should not do that. Do this instead.
NSManagedObjectContext *mainMOC = self.managedObjectContext;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
Make sure you access the MOC in a performBlock. For example,
[moc performBlock:^{
// Anything at all involving this MOC or any of its objects
}];
When the json data has completed I attempt to peform a save operation
with the following macro:
Consider saving with something like this. Your completion block will be called when the save has finished.
- (void)saveMOC:(NSManagedObjectContext*)moc
completion:(void(^)(NSError *error))completion {
[moc performBlock:^{
NSError *error = nil;
if ([moc save:&error]) {
if (moc.parentContext) {
return [self saveMOC:moc.parentContext completion:completion];
}
}
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(error);
});
}
}];
}
[self saveMOC:moc completion:^(NSError *error) {
// Completion handler is called from main-thread, after save has finished
if (error) {
// Handle error
} else {
}
}];
EDIT
This code will crash if moc.parentContext is main concurrency type. –
Mundi
There is no inherent reason that the code I posted should cause a crash with a parent MOC of NSMainQueueConcurrencyType. It has supported being a parent context ever since parent/child was added to Core Data.
Maybe I was missing a typo, so I copy/paste saveMOC:completion: straight from this answer, and wrote the following test helper.
- (void)testWithChildConcurrencyType:(NSManagedObjectContextConcurrencyType)childConcurrencyType
parentConcurrencyType:(NSManagedObjectContextConcurrencyType)parentConcurrencyType {
NSAttributeDescription *attr = [[NSAttributeDescription alloc] init];
attr.name = #"attribute";
attr.attributeType = NSStringAttributeType;
NSEntityDescription *entity = [[NSEntityDescription alloc] init];
entity.name = #"Entity";
entity.properties = #[attr];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init];
model.entities = #[entity];
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
[psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL];
NSManagedObjectContext *parent = [[NSManagedObjectContext alloc] initWithConcurrencyType:parentConcurrencyType];
parent.persistentStoreCoordinator = psc;
NSManagedObjectContext *child = [[NSManagedObjectContext alloc] initWithConcurrencyType:childConcurrencyType];
child.parentContext = parent;
NSManagedObject *obj = [NSEntityDescription insertNewObjectForEntityForName:#"Entity" inManagedObjectContext:child];
[obj setValue:#"value" forKey:#"attribute"];
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:#"save from %# to %# finished", concurrencyTypeString(childConcurrencyType), concurrencyTypeString(parentConcurrencyType)]];
[self saveMOC:child completion:^(NSError *error) {
// Verify data saved all the way to the PSC
NSManagedObjectContext *localMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
localMoc.persistentStoreCoordinator = psc;
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
XCTAssertEqualObjects(#"value", [[[localMoc executeFetchRequest:fr error:NULL] firstObject] valueForKey:#"attribute"]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
And then, I wrote a test for each possible parent/child relationship.
- (void)testThatDoingRecursiveSaveFromPrivateToPrivateWorks {
[self testWithChildConcurrencyType:NSPrivateQueueConcurrencyType
parentConcurrencyType:NSPrivateQueueConcurrencyType];
}
- (void)testThatDoingRecursiveSaveFromPrivateToMainWorks {
[self testWithChildConcurrencyType:NSPrivateQueueConcurrencyType
parentConcurrencyType:NSMainQueueConcurrencyType];
}
- (void)testThatDoingRecursiveSaveFromMainToPrivateWorks {
[self testWithChildConcurrencyType:NSMainQueueConcurrencyType
parentConcurrencyType:NSPrivateQueueConcurrencyType];
}
- (void)testThatDoingRecursiveSaveFromMainToMainWorks {
[self testWithChildConcurrencyType:NSMainQueueConcurrencyType
parentConcurrencyType:NSMainQueueConcurrencyType];
}
So, what am I missing?
As I write this, I am reminded of a 360iDev presentation where the presenter said that you can't call performBlock on a NSMainQueueConcurrencyType context. At the time, I thought he just misspoke, meaning confinement, but maybe there is some confusion in the community about this.
You can't call performBlock on a NSConfinementConcurrencyType MOC, but performBlock is fully supported for NSMainQueueConcurrencyType.

NSTask Blocking My UI when NSTask encounter large data

I want to update my nstextview with the data generated during nstask execution(Ipa Generation). But when i run my code to execute nstask, in the middle my nstask blocks my ui but the task continues to execute. At last when nstask terminates my ui starts working properly.
This is my code where i am running my nstask:
dispatch_queue_t taskQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
NSTask *task=[[NSTask alloc]init];
dispatch_async(taskQueue, ^{
#try {
[task setArguments:arguments];
[task setLaunchPath: launchPath];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(taskCompletion:) name: NSTaskDidTerminateNotification object:task];
// Output Handling
NSPipe *outputPipe = [[NSPipe alloc] init];
outputFileHandle = [[NSFileHandle alloc]init];
[task setStandardOutput:outputPipe];
outputFileHandle=[outputPipe fileHandleForReading];
[outputFileHandle waitForDataInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification *notification){
NSData *output = [outputFileHandle availableData];
NSString *outStr = [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding];
dispatch_sync(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(taskCompletion:) name: NSTaskDidTerminateNotification object:task];
NSLog(#"%#",outStr);
NSString *textViewData =[self.outputView string];
self.outputView.string = [textViewData stringByAppendingString:[NSString stringWithFormat:#"\n%#", outStr]];
// Scroll to end of outputText field
NSRange range;
range = NSMakeRange([self.outputView.string length], 0);
[self.outputView scrollRangeToVisible:range];
});
[outputFileHandle waitForDataInBackgroundAndNotify];
}];
[task launch];
[task waitUntilExit];
}
#catch (NSException *exception) {
NSLog(#"Problem Running Task: %#", [exception description]);
}
#finally {
NSLog(#"i m in finally xbuild");
}
});
I really stuck in that .Your suggestions will be helpful for me.

How to prevent race conditions when merging changes across threads?

A typical setup: we have a main thread with a mainMOC and a background thread with its own backgroundMOC. The background thread performs read-only operations on the backgroundMOC by dispatching blocks to a backgroundQueue.
The backgroundMOC needs to merge the changes from the mainMOC so we register for NSManagedObjectContextDidSaveNotification and then do something like
- (void)mainMocDidSave:(NSNotification *)notification {
dispatch_async(backgroundQueue, ^{
[backgroundMoc mergeChangesFromContextDidSaveNotification:notification];
});
}
Let's say the user deletes an object in the mainMOC. The code above does not seem safe to me, since the merge will be done at some point in the future. Until the merge is done, there might still be blocks on the backgroundQueue that are trying to use the deleted object.
The obvious solution would be to use dispatch_sync instead (or performBlockAndWait, performSelector:OnThread:...) instead. From the code snippets I see on the interwebs, this seems to be what everybody is doing. But I'm not comfortable with this solution either.
The name NSManagedObjectContextDidSaveNotification implies that the save has already happened when the notification is delivered. So the corresponding row has already been deleted from the underlying database (assuming an sqlite store). dispatch_sync will have to wait for other blocks on the queue to finish before it can merge the changes, and these other blocks could still try to work with the deleted object, leading to an NSObjectInaccessibleException.
It seems to me that the correct way to merge changes from one thread/queue to another would be to
Subscribe to NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification on the background thread.
On NSManagedObjectContextWillSaveNotification: empty the backgroundQueue and suspend any operations that dispatch new blocks to the queue.
On NSManagedObjectContextDidSaveNotification: merge the changes synchronously.
Resume normal operation on the background queue.
Is this the correct approach or am I missing something?
I use the following structure in two projects in which I experienced similar troubles as you did. First of all I use singleton service to ensure there is only one background thread merging and reading changes.
AppDelegate.m
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
// It is crucial to use the correct concurrency type!
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
- (void)saveContext {
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
else {
[[NSNotificationCenter defaultCenter] postNotificationName:#"ParentContextDidSaveNotification" object:nil];
}
}
}
BackgroundService.m
- (id)init {
self = [super init];
if (self) {
[self managedObjectContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(parentContextDidSave) name:#"ParentContextDidSaveNotification" object:nil];
}
return self;
}
- (NSManagedObjectContext *)managedObjectContext {
if (!_managedObjectContext) {
// Again, make sure you use the correct concurrency type!
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_managedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
}
return _managedObjectContext;
}
- (BOOL)saveContext {
#synchronized(self) {
BOOL successful = YES;
// Bad practice, process errors appropriately.
[[self managedObjectContext] save:nil];
[[[self managedObjectContext] parentContext] performBlock:^{
[(AppDelegate *)[[UIApplication sharedApplication] delegate] saveContext];
}];
return successful;
}
}
- (void)parentContextDidSave {
[[self managedObjectContext] reset];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ManagedObjectContextResetNotification" object:nil];
}

CoreData Fetch Crashes if used with dispatch_async

I have a core-data app that runs without crashing if I perform a fetch inside viewDidLoad like this:
- (void) performCoreDataFetch {
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
exit(-1); // Fail
}
}
- (void)viewDidLoad {
[super viewDidLoad];
[self performCoreDataFetch];
}
The only problem with the above way of performing fetch is if the data to be returned is big, it freezes the app for a few seconds (but does return correct result without crashing every single time), so to avoid that I decided to use dispatch_async (code shown below) and call [self performCoreDataFetch] inside it.
But if I run this same [self performCoreDataFetch] inside dispatch_sync within viewDidLoad, like shown below,:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performCoreDataFetch];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
});
Calling [self performCoreDataFetch] within dispatch_async crashes the app randomly saying "-[NSFetchRequest fetchLimit]: message sent to deallocated instance"
My fetchedResultsController method looks like this:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create and configure a fetch request with the Organization entity
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Organization" inManagedObjectContext:managedObjectContext];
request.fetchBatchSize = 20;
// create sortDescriptor array
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES selector:#selector(caseInsensitiveCompare:)];
NSArray *sortDescriptorArray = [NSArray arrayWithObjects:nameDescriptor, nil];
request.sortDescriptors = sortDescriptorArray;
NSPredicate *predicate = nil;
predicate = [NSPredicate predicateWithFormat:#"state LIKE %#", filterByState];
[request setPredicate:predicate];
// Create and initialize the fetchedResultsController
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc ] initWithFetchRequest:request managedObjectContext:managedObjectContext sectionNameKeyPath:#"uppercaseFirstLetterOfName" cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
// Memory management
filterByState = nil;
// [sortDescriptorArray release];
[nameDescriptor release];
// [predicate release];
[request release];
[aFetchedResultsController release];
return fetchedResultsController;
}
Core data is not thread safe if you perform a fetch for a fetchedResultsController. It makes sense, as the fetchedResultsController is the datasource of your UI. Rather than perform a fetch, set your fetchedResultsController to nil and reload your tableView.
Core data is not thread save. To be more exact, the NSManagedObjectContext is not save. All NSManagedObject belong to a particular NSManagedObjectContext and they are not interchangeable.
Pre IOS 5 you need to put really really complicated method. Basically each thread require it's own NSManagedContext
After IOS5, you can do:
__managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
Then you can do
[__managedObjectContext performBlock ^{
//Some really long operation
}]
on any thread that is not main thread.
That will do it on a different thread however in a thread save way. Basically core data will put your operation into queues and it will execute that one by one locking the managedObjectContext for each operation.

UIManagedDocument - How to deal with UIDocumentStateSavingError?

I am working on my first iCloud App. After working for a while the app cannot access a UIManagedDocument any more due to an "UIDocumentStateSavingError". Is there any way to actually find out what error occurred?
This is my code to create the UIManagedDocument:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
iCloudURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
if (iCloudURL == nil) {
dispatch_async(dispatch_get_main_queue(), ^{
[self iCloudNotAvailable];
});
return;
}
iCloudDocumentsURL = [iCloudURL URLByAppendingPathComponent:#"Documents"];
iCloudCoreDataLogFilesURL = [iCloudURL URLByAppendingPathComponent:#"TransactionLogs"];
NSURL *url = [iCloudDocumentsURL URLByAppendingPathComponent:#"CloudDatabase"];
iCloudDatabaseDocument = [[UIManagedDocument alloc] initWithFileURL:url];
NSMutableDictionary *options = [NSMutableDictionary dictionary];
NSString *name = [iCloudDatabaseDocument.fileURL lastPathComponent];
[options setObject:name forKey:NSPersistentStoreUbiquitousContentNameKey];
[options setObject:iCloudCoreDataLogFilesURL forKey:NSPersistentStoreUbiquitousContentURLKey];
iCloudDatabaseDocument.persistentStoreOptions = options;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentContentsChanged:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:iCloudDatabaseDocument.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentStateChanged:) name:UIDocumentStateChangedNotification object:iCloudDatabaseDocument];
if ([[NSFileManager defaultManager] fileExistsAtPath:[iCloudDatabaseDocument.fileURL path]]) {
// This is true, the document exists.
if (iCloudDatabaseDocument.documentState == UIDocumentStateClosed) {
[iCloudDatabaseDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
[self documentConnectionIsReady];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self connectionError:iCloudConnectionErrorFailedToOpen];
});
}
}];
} else if (iCloudDatabaseDocument.documentState == UIDocumentStateNormal) {
...
}
} else {
...
}
});
The Document already exists and thus openWithCompletionHandler: is called on the document. This fails and the UIDocumentStateChangedNotification is fired which shows a document states of 5:
UIDocumentStateClosed and
UIDocumentStateSavingError
After this the completion block gets called. What is correct way to proceed from here? Is there any way to find out what went wrong and what kind of error occurred?
I tried to re-open the document in the completion block but the result is the same.
I guess I could solve the problem by just deleting the file and recreate it. But this is obviously not an option once the app will be out in the store. I would like to know what is going wrong and give the user an appropriator way to handle the problem.
I already checked other questions here handling the UIDocumentStateSavingError (there a not a lot of them) but the seem not to be applicable for the problem here.
Any idea how I can find out what the problem is? I cannot belive that the API tells you "Something went wrong during saving but I will not tell you what!"
You can query the documentState in the completion handler. Unfortunately, if you want the exact error, the only way to get it is to subclass and override handleError:userInteractionPermitted:
Maybe something like this would help (typed freehand without compiler)...
#interface MyManagedDocument : UIManagedDocument
- (void)handleError:(NSError *)error
userInteractionPermitted:(BOOL)userInteractionPermitted;
#property (nonatomic, strong) NSError *lastError;
#end
#implementation MyManagedDocument
#synthesize lastError = _lastError;
- (void)handleError:(NSError *)error
userInteractionPermitted:(BOOL)userInteractionPermitted
{
self.lastError = error;
[super handleError:error
userInteractionPermitted:userInteractionPermitted];
}
#end
Then in you can create it like this...
iCloudDatabaseDocument = [[UIManagedDocument alloc] initWithFileURL:url];
and use it in the completion handler like this...
[iCloudDatabaseDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
[self documentConnectionIsReady];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self connectionError:iCloudConnectionErrorFailedToOpen
withError:iCloudDatabaseDocument.lastError];
});
}
}];
Based on #JodyHagins excellent snippet, I have made a UIDocument subclass.
#interface SSDocument : UIDocument
- (void)openWithSuccess:(void (^)())successBlock
failureBlock:(void (^)(NSError *error))failureBlock;
#end
#interface SSDocument ()
#property (nonatomic, strong) NSError *lastError;
#end
#implementation SSDocument
- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted {
self.lastError = error;
[super handleError:error userInteractionPermitted:userInteractionPermitted];
}
- (void)clearLastError {
self.lastError = nil;
}
- (void)openWithSuccess:(void (^)())successBlock failureBlock:(void (^)(NSError *error))failureBlock {
NSParameterAssert(successBlock);
NSParameterAssert(failureBlock);
[self clearLastError];
[self openWithCompletionHandler:^(BOOL success) {
if (success) {
successBlock();
} else {
NSError *error = self.lastError;
[self clearLastError];
failureBlock(error);
}
}];
}
#end

Resources