How to wait for iOS methods with completion-blocks to finish

iOS completion blocks rule. A little bit of code that runs after a (normally slow) task finishes, with full access to the classes member variables, and even read-only access to the enclosing method’s local variables.

However, sometimes you want these calls to be blocking.

*shock horror* you may be thinking. Why hasn’t he re-designed his app flow to take advantage of these new methods? (this was the general response that one guy got to his question “How to wait for methods with result/completion blocks to finish?“).

Well the answer to that question is that I already designed my UI to be fast and responsive, with cancellable actions – before it was cool I guess. So all my code is already run in worker threads. So now when I want to implement something like ALAssetManager image loading, I want to add it to code that is already nicely multi-threaded, and designed to support other methods of loading images (that use blocking reads, rather than the newer completion block design). I’d rather not totally mess up the flow of my logic, and since it’s already threaded, it’s perfectly fine to just block and wait for the asset manager to return.

So how do you do this?

The answer is NSLock. Specifically NSConditionLock. Basically you create a lock with the condition “pending tasks”. Then in the completion and error blocks of assetForURL, you unlock it with the “all complete” condition. With this in place, after you call assetForURL, simply call lockWhenCondition using your “all complete” identifier, and you’re done (it will wait until the blocks set that condition)!

A big caveat that applies to ASAssetManager (but not always other iOS APIs using a similar block-callback structure) is that runs some code on the main thread. So to use this system you *must* call it from a thread. As I said in my pre-amble, the reason I want to block is that this is already in a worker thread anyway, so it shouldn’t be a problem (if it is for you, then reconsider your design – it’s not a good idea to block up the main thread).

Here’s some rough code as an example. I haven’t made it into a utility for you to use, as you probably want to understand what’s going on here, rather than dragging and dropping my code into your project.


// --------------- .h file

	// class members in the header file (can't be local as then the blocks wouldn't be able to use them

	NSConditionLock* albumReadLock;
	NSData* defaultRepresentationData;


// --------------- .m file

// NSConditionLock values
enum {
    WDASSETURL_PENDINGREADS = 1,
    WDASSETURL_ALLFINISHED = 0
};


// loads the data for the default representation from the ALAssetLibrary
- (NSData) loadDataForDefaultRepresentation:(NSURL*)assetURL
{
	// this method *cannot* be called on the main thread as ALAssetLibrary needs to run some code on the main thread and this will deadlock your app if you block the main thread...
	// don't ignore this assert!!
	NSAssert(![NSThread isMainThread], @"can't be called on the main thread due to ALAssetLibrary limitations");		

	// sets up a condition lock with "pending reads"
	albumReadLock = [[NSConditionLock alloc] initWithCondition:WDASSETURL_PENDINGREADS];
	
	// the result block
    ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
    {
        ALAssetRepresentation *rep = [myasset defaultRepresentation];

		NSLog(@"GOT ASSET, File size: %f", [rep size] / (1024.0f*1024.0f)); 
		
		uint8_t* buffer = malloc([rep size]);
		
		NSError* error = NULL;
		NSUInteger bytes = [rep getBytes:buffer fromOffset:0 length:[rep size] error:&error];
		
		if (bytes == [rep size])
		{
			NSLog(@"Asset %@ loaded from Asset Library OK", self.assetURL); 
			defaultRepresentationData = [[NSData dataWithBytes:buffer length:bytes] retain];
		}
		else
		{
			NSLog(@"Error '%@' reading bytes from asset: '%@'", [error localizedDescription], self.assetURL);
		}
		
		free(buffer);
		
		// notifies the lock that "all tasks are finished"
		[albumReadLock lock];
		[albumReadLock unlockWithCondition:WDASSETURL_ALLFINISHED];
    };
	
    //
    ALAssetsLibraryAccessFailureBlock failureblock  = ^(NSError *myerror)
    {
		NSLog(@"NOT GOT ASSET"); 
		
        NSLog(@"Error '%@' getting asset from library", [myerror localizedDescription]);
		
		// important: notifies lock that "all tasks finished" (even though they failed)
		[albumReadLock lock];
		[albumReadLock unlockWithCondition:WDASSETURL_ALLFINISHED];
    };
	
	// schedules the asset read
	ALAssetsLibrary* assetslibrary = [[[ALAssetsLibrary alloc] init] autorelease];
	
	[assetslibrary assetForURL:assetURL 
				   resultBlock:resultblock
				  failureBlock:failureblock];

	// non-busy wait for the asset read to finish (specifically until the condition is "all finished")
	[albumReadLock lockWhenCondition:WDASSETURL_ALLFINISHED];
	[albumReadLock unlock];
	
	// cleanup
	[albumReadLock release];
	albumReadLock = nil;

	// returns the image data, or nil if it failed
	return defaultRepresentationData;
}

11 comments on “How to wait for iOS methods with completion-blocks to finish

  1. Thanks a lot.
    I was wondering how this can be adopted to the enumerateAssetsUsingBlock method.

    Basically what i want to do is upload all the thumbnail of Assets in a asset group. Rather than making 20-30 call to server, i feel it is better if i can do it one go.

    Is there someway i can get all the Assets in an array.

  2. Hi,
    this seems not to work for me.
    I tried a simple
    [library assetForURL:url resultBlock:resultBlock failureBlock:failureBlock];
    with your lock/unlock code in the blocks and the lockWhenCondition code after “assetForURL”.
    This waits forever. I don’t know why, please help.
    My code is called from an NSOperationQueue.

  3. I found it!
    [queue waitUntilAllOperationsAreFinished] is evil, if called from the main thread.
    The operations get dispatched to other threads but the queue itself blocks the mainThread, which -as you pointed out- the ALAssetManage do not like.

  4. Hey, Arno, could you share your solution for that?

    I also have this code called from an NSOperationQueue and it doesn’t seem to work =\
    Basically, the resultBlock of assetForURL:… never gets called.

    Actually, I have a problem when I’m trying to get too much asset images from library simultaneously using nsoperationqueue. And I was trying to solve that using the condition locks, but I can’t get that working anyhow=\

    Thanks,
    Ivan

  5. Great tutorial. How can you run the section of code on a background thread and be able to return a value? Both the performSelectorInBackground: and dispatch_async methods have return type void.

    Can anyone help me out here?

  6. Pingback: Making objective-c blocks synchronous | Coder Cowboy

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>