Thursday 4 December 2014

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;
} 

Thanks to http://omegadelta.net/2011/05/10/how-to-wait-for-ios-methods-with-completion-blocks-to-finish/

No comments:

Post a Comment