I have situation where I am iterating my array objects using enumerateObjectsUsingBlock, and I need to wait for a completion in the iteration then it should execute further code, how can I achieve this, or if any alternate I should use, below is my code
[arrPendingQueue enumerateObjectsUsingBlock:^(PendingQueues *obj, NSUInteger idx, BOOL *stop) {
//Fetch static google map based on coordinates
[self generateMapImage:obj.postObj completion:^(UIImage *image) {
NSString *path = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:#"camera"];
NSString *resultPath = [path stringByAppendingPathComponent:[NSString stringWithFormat:#"map_%f.jpg",[[Constants getSystemDateInLocalTimeZone] timeIntervalSince1970]]];
[[NSFileManager defaultManager] createFileAtPath:resultPath contents:UIImageJPEGRepresentation(image, 1.0) attributes:nil];
postObj.imagepath = [#"camera" stringByAppendingPathComponent:[resultPath lastPathComponent]];
[DataManager saveObject:postObj];
}];
//After the completion I want to upload the data here to server
});
Basically, I am trying to download static google map from coordinate in function generateMapImage and I want the loop to wait till completion called..
For that I tried to use dispatch_semaphore_create like
[arrPendingQueue enumerateObjectsUsingBlock:^(PendingQueues *obj, NSUInteger idx, BOOL *stop) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self generateMapImage:obj.postObj completion:^(UIImage *image) {
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});
But it doesn't work it doesn't wait, and executes the code before completion completed.
How to solve, please help.
Thanks.
To queue up execution of a series of a blocks, you can simply start the new block in the completion handler of the old block:
NSEnumerator *objectEnum = [arrPendingQueue objectEnumerator];
__block void (^handler) ();
handler = ^(UIImage *image)
{
// Handle image
object = [objectEnum nextObject];
if( object == nil )
{
// completion of all operations
}
else
{
// Start next operation
[self generateMapImage:object.postObj completion:handler];
}
};
[self generateMapImage:obj.postObj completion:handler];
}
A more classical or elegant way would be to use a Y-combinator. But it is not necessary. Just keep in mind, that local scope variables are cleaned up when losing their scope. handler is not retained. So maybe you have to put it into an ivar.
However, I see -generateMapImage:completion is written by you. I cannot see the content, but if you do a async call there and you can set the queue used for the call, simply set it to a serial queue. In such a case your requests will be executed serial automatically.
Related
I am using generateCGImagesAsynchronouslyForTimes to make some images and save them to a NSMutableArray, now when the function generateCGImagesAsynchronouslyForTimes finishes I want to use the image in this array, how can I have the code I want to exectue after all the images have been generated to finish. I would just put it in the completionHandler code block, but I don't want it run multiple times I just want to run it once, after this method has finished.
EDIT
This is all inside - (BFTask *)createImage:(NSInteger)someParameter {
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:passsedAsset];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,
AVAssetImageGeneratorResult result, NSError *error) {
if (result == AVAssetImageGeneratorSucceeded) {
UIImage *img = [UIImage imageWithCGImage:image];
NSData *imgData = UIImageJPEGRepresentation(img, 1.0);
UIImage *saveImage = [[UIImage alloc] initWithData:imgData];
[mutaleArray addObject:saveImage];
//I get Assigment to read only property error on line below
completionSource.task = saveImage;
}
]};
What should i be assigning that to?
The two approaches I would consider first are NSOperationQueue (you can detect when it's empty) or the easier choice of using the Bolts framework.
Bolts allows you to create an array of tasks that all run asynchronously and then once they're finished it goes on to the next bit.
Let me get a link...
Here you go... https://github.com/BoltsFramework
You can also get this through cocoapods which makes everything much easier.
An example of how bolts works...
At the moment you will have a function that creates an image asynchronously. Something like... - (UIImage *)createImage: (id)someParameter; well now you can do this...
- (BFTask *)createImage:(NSInteger)someParameter
{
BFTaskCompletionSource *completionSource = [BFTaskCompletionSource taskCompletionSource];
//create your image asynchronously and then set the result of the task
someAsyncMethodToCreateYourImageWithACompletionBlock...^(UIImage *createdImage){
// add the images here...
[self.imageArray addObject:createdImage];
// the result doesn't need to be the image it just informs
// that this one task is complete.
completionSource.result = createdImage;
}
return completionSource.task;
}
Now you have to run the tasks in parallel...
- (void)createAllTheImagesAsyncAndThenDoSomething
{
// create the empty image array here
self.imageArray = [NSMutableArray array];
NSMutableArray *tasks = [NSMutableArray array];
for (NSInteger i=0 ; i<100 ; ++i) {
// Start this creation immediately and add its task to the list.
[tasks addObject:[self createImage:i]];
}
// Return a new task that will be marked as completed when all of the created images are finished.
[[BFTask taskForCompletionOfAllTasks:tasks] continueWithBlock:^id(BFTask *task){
// this code will only run once all the images are created.
// in here self.imageArray is populated with all the images.
}
}
Assuming that generateCGImagesAsynchronouslyForTimes:completionHandler: calls its completion handlers sequentially (that seems reasonable, but the docs don't explicitly promise), then this is very simple. Just set a __block variable to the count of your times and decrement it once per completion. When it's zero, call your other function.
__block NSInteger count = [times count];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,
AVAssetImageGeneratorResult result, NSError *error) {
... Do all the stuff ...
if (--count <= 0) {
finalize()
}
If generateCGImagesAsynchronouslyForTimes: actually does work in parallel and so might call the completion handlers in parallel, then you can handle all of this with dispatch groups.
dispatch_group_t group = dispatch_group_create();
//
// Enter the group once for each time
//
[times enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
dispatch_group_enter(group);
}];
//
// This local variable will be captured, so you don't need a property for it.
//
NSMutableArray *results = [NSMutableArray new];
//
// Register a block to fire when it's all done
//
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(#"Whatever you want to do when everything is done.");
NSLog(#"results is captured by this: %#", results);
});
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:nil];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,
AVAssetImageGeneratorResult result, NSError *error)
{
if (result == AVAssetImageGeneratorSucceeded) {
//
// Create saveImage
//
id saveImage = #"";
//
// Update external things on a serial queue.
// You may use your own serial queue if you like.
//
dispatch_sync(dispatch_get_main_queue(), ^{
[results addObject:saveImage];
});
//
// Signal we're done
//
dispatch_group_leave(group);
}
}];
I am new to blocks and am trying to figure out how to wait for the block to finish before performing my action (in this case a nslog) So how can I wait till the block is done before performing this nslog in the code below: NSLog(#"convertedPhotos::%#",convertedImages);
convertedImages = [[NSMutableArray alloc] init];
for (NSDictionary *photo in photos) {
// photo is a dictionary containing a "caption" and a "urlRep"
[photoUrls addObject:photo[#"urlRep"]];
}
if (photoUrls.count) {
for (id photos in photoUrls){
NSString *urlString = photos;
[self base64ImageAtUrlString:urlString result:^(NSString *base64) {
[jsonWithPhotos setObject:convertedImages forKey:#"photo64"];
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonWithPhotos
options:NSJSONWritingPrettyPrinted // Pass 0 if you don't care about the readability of the generated string
error:&error];
if (! jsonData) {
NSLog(#"Got an error: %#", error);
} else {
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSLog(#"json::%#",jsonString);
}
}];
}
}
else {
NSLog(#"where are my urls?");
}
NSLog(#"convertedPhotos::%#",convertedImages);
}
}
this method/block is called from above
- (void)base64ImageAtUrlString:(NSString *)urlString result:(void (^)(NSString *))completion {
NSURL *url = [NSURL URLWithString:urlString];
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library assetForURL:url resultBlock:^(ALAsset *asset) {
// borrowing your code, here... didn't check it....
ALAssetRepresentation *representation = [asset defaultRepresentation];
CGImageRef imageRef = [representation fullResolutionImage];
//TODO: Deal with JPG or PNG
NSData *imageData = UIImageJPEGRepresentation([UIImage imageWithCGImage:imageRef], 0.1);
NSString *base64 = [imageData base64EncodedString];
completion(base64);
[convertedImages addObject:base64];
// NSLog(#"converted::%#",convertedImages);
} failureBlock:^(NSError *error) {
NSLog(#"that didn't work %#", error);
}];
}
I would use NSOperationQueue (or dispatch queue) and NSCondition (or dispatch group) to wait for operations to complete. It is also important to wrap blocks in #autoreleasepool to flush memory once you do not need it if you work with memory consuming objects like NSData.
example:
// create results array
__block NSMutableArray* results = [NSMutableArray new];
// create serial queue
dispatch_queue_t queue = dispatch_queue_create("myQueue", 0);
for(NSInteger i = 0; i < 10; i++) {
// enqueue operation in queue
dispatch_async(queue, ^{
// create semaphore
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// do something async, I do use another dispatch_queue for example
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// wrap in autoreleasepool to release memory upon completion
// in your case wrap the resultBlock in autoreleasepool
#autoreleasepool {
// here for example the nested operation sleeps for two seconds
sleep(2);
// add the operation result to array
// I construct an array of strings for example
[results addObject:[NSString stringWithFormat:#"Operation %d has finished.", i]];
// signal that nested async operation completed
// to wake up dispatch_semaphore_wait below
dispatch_semaphore_signal(sema);
}
});
// wait until the nested async operation signals that its finished
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(#"Finished single operation.");
});
}
// will be called once all operations complete
dispatch_async(queue, ^{
NSLog(#"Finished all jobs.");
NSLog(#"Results: %#", results);
});
For any non-main queue use semaphores
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// some serious stuff here
...
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
In case you want to wait for async task execution being in the main queue - you wouldn't probably want to block it while waiting for a semaphore.
I use this construction which doesn't freeze the UI for the main queue only.
__block BOOL flag = NO;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// some serious stuff here
...
flag = YES;
});
// Run until 'flag' is not flagged (wait for the completion block to finish executing
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) && !flag){};
Your best option is not to use blocks directly. Instead, create instances of NSBlockOperation and add them to an operation queue. Then create one more NSBlockOperation and make it dependent upon all of the other operations. This ensures that the last operation is only run after all others are completed and allows you to control how many operations will run at any one time.
If you have nested block calls, or some API that you can't change to enable this then you can still do it if you create an NSOperation subclass such that the operation does not complete until all of the asynchronous operations are complete. Take a look at dribin.org concurrent operations.
I am trying to iterate all images stored in a Box account.
The following code isn't correct and can't seem to find the error.
The problem seems to be waiting for all of the asynchronous recursive calls to complete in order to know when there are no more images to be fetched in order to signal completion
-(void)enumerateFiles
{
[self recursiveEnumerateFilesAtPath:#"0" completion:nil];
}
-(void)recursiveEnumerateFilesAtPath:(NSString *)folderID completion:(void(^)())block
{
static NSArray *imageExtensions;
imageExtensions = #[#"jpg",#"jpeg",#"png"];
[self enumerateFilesAtPath:folderID completion:^(BoxCollection *collection) {
NSUInteger numberOfItems = collection.numberOfEntries;
for (int i = 0; i < numberOfItems; i++) {
id model = [collection modelAtIndex:i];
if ([model isKindOfClass:[BoxFolder class]])
{
BoxFolder *folder = (BoxFolder *)model;
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[self recursiveEnumerateFilesAtPath:folder.modelID completion:^{
dispatch_group_leave(group);
}];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
} else if ([model isKindOfClass:[BoxItem class]])
{
BoxItem *item = (BoxItem *)model;
NSString *extension = [[item.name pathExtension] lowercaseString];
if ([imageExtensions containsObject:extension])
{
[self.items addObject:model];
}
}
}
if (block)
{
block();
}
}];
}
-(void)enumerateFilesAtPath:(NSString *)folderID completion:(void(^)(BoxCollection *collection))block
{
BoxCollectionBlock success = ^(BoxCollection *collection)
{
block(collection);
};
BoxAPIJSONFailureBlock failure = ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSDictionary *JSONDictionary)
{
block(nil);
};
[[BoxSDK sharedSDK].foldersManager folderItemsWithID:folderID requestBuilder:nil success:success failure:failure];
}
The key to a viable approach is to make the inner loop, that is
for (int i = 0; i < numberOfItems; i++) {..}
a construct which sequentially invokes a number of asynchronous tasks and is itself asynchronous. The asynchronous task should be a block (^) which will invoke the outer method and will capture state so that its outer method recursiveEnumerateFilesAtPath: can be invoked in a recursive manner.
Note that this is not a recursion we know from functions, where a function calls itself before it returns, and where the state is hold on the program stack. Here, we rather have an iteration (asynchronous loop) - and there's no state on the stack, instead the state required for the "recursion" is hold in the variables captured in the blocks, and these reside on the heap.
That is, you need to transform the "for loop" (for (int i = 0; i < numberOfItems; i++) {..}) into some asynchronous method, e.g:
- (void) forEach:(NSArray*)objects applyTask:(task_t) completion:(completion_t)completionHandler;
where task is asynchronous, has a completion handler, and (conditionally) calls the outer method recursiveEnumerateFilesAtPath:.
This forEach construct could be implemented using a NSOperationQueue, whose max concurrent operations is set to one, and where the task is a NSOperation with a completion handler. Alternatively - and simpler in this case, it can be implemented using dispatch queues and a dispatch group. Or - even more simpler, it can be implemented using an "asynchronous loop". I'll show a possible implementation, see "NSArray Category forEachApplyTask:completion: below.
Now, suppose there is a Category for NSArray with an asynchronous forEachApplyTask:completion: method:
#interface NSArray (AsyncExtension)
- (void) forEachApplyTask:(unary_async_t)task completion:(completion_t) completion;
#end
This method takes a block of type unary_async_t as first parameter:
typedef void (^unary_async_t)(id input, completion_t completion);
and a completion handler as second parameter:
typedef void (^completion_t)(id result);
This typedef of a completion handler, is generic and can be used for all completion handler variants.
Let's suppose, the block task will be sequentially applied for each element in the receiver (the array). And suppose, this array is an array of "model"s in your original code -- you just need to create an array of "model"s out from the BoxCollection object.
When finished, the completion handler passes a result, which is an array containing the result of each task in the same order.
This task executes the block ({..}) in your former for loop.
Your method recursiveEnumerateFilesAtPath: will become inevitable asynchronous (since to invokes an asynchronous method in its implementation), and thus gets a completion handler parameter as well.
With the given assumptions made above, you can implement your problem as shown below:
-(void)recursiveEnumerateFilesAtPath:(NSString *)folderID
completion:(completion_t)finalCompletionHandler
{
[self enumerateFilesAtPath:folderID completion:^(BoxCollection *collection) {
NSArray* models = ...; // create an NSArray from the collection
[models forEachApplyTask:^(id model, completion_t taskCompletionHandler) {
if ([model isKindOfClass:[BoxFolder class]]) {
BoxFolder *folder = (BoxFolder *)model;
[self recursiveEnumerateFilesAtPath:folder.modelID completion:^(id result){
// result should be #"finished folder"
taskCompletionHandler(#"folder"); // taskCompletionHandler will never be nil
}];
}
else if ([model isKindOfClass:[BoxItem class]]) {
BoxItem *item = (BoxItem *)model;
NSString *extension = [[item.name pathExtension] lowercaseString];
if ([imageExtensions containsObject:extension]) {
[self.items addObject:model];
}
taskCompletionHandler(#"item");
}
}
completion:^(id array){
// this folder is finished. array may be for example #[#"folder", #"item", #"item"]
if (finalCompletionHandler) {
finalCompletionHandler(#"finished folder");
}
}];
}];
}
Caution: not tested, you may experience issues!
Note, that everything runs asynchronous, and that there is no recursion whose state is put on the stack. The required state for recursively iterating the folders is put on the heap, enclosed in captured variables in the block.
The NSArray Category method forEachApplyTask:completion: is a reusable component, which can be utilized in many cases. One can implement it as follows:
Implementation of Category NSArray
/**
Objective:
Asynchronously transform or process an array of items - one after the
other and return the result of each transform in an array.
Synopsis:
void transform_each(NSArray* inArray, unary_async_t task, completion_t completion);
*/
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>
/**
Typedef of a generic completion handler for an asynchronous task.
The parameter _result_ is the eventual result of the asynchronous
task. In case the task has been failed _result_ SHALL be an
NSError object.
*/
typedef void (^completion_t)(id result);
/**
Typedef for an asynchronous "transform" function. It's an
unary block taking an input as parameter and signals
the eventual result via a completion handler.
*/
typedef void (^unary_async_t)(id input, completion_t completion);
/**
`transform_each` sequentially applies an asynchronous transform function
to each object in the input array _inArray_ and signals the result as an
array containing the result of each transform applied to the input object.
Function `transform_each` is itself a asynchronous function, that is,
its eventual result will be signaled to the client through a completion
handler.
The result array contains the transformed objects in order of the
corresponding input array.
*/
void transform_each(NSArray* inArray, unary_async_t task, completion_t completion);
// implementation
static void do_each(NSEnumerator* iter, unary_async_t task, NSMutableArray* outArray, completion_t completion)
{
id obj = [iter nextObject];
if (obj == nil) {
if (completion)
completion([outArray copy]);
return;
}
task(obj, ^(id result){
[outArray addObject:result];
do_each(iter, task, outArray, completion);
});
}
void transform_each(NSArray* inArray, unary_async_t task, completion_t completion) {
NSMutableArray* outArray = [[NSMutableArray alloc] initWithCapacity:[inArray count]];
NSEnumerator* iter = [inArray objectEnumerator];
do_each(iter, task, outArray, completion);
}
/*******************************************************************************
Example
*******************************************************************************/
// A Category for NSArray
#interface NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(unary_async_t) task completion:(completion_t) completion;
#end
#implementation NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(unary_async_t) task completion:(completion_t) completion {
transform_each(self, task, completion);
}
#end
See also: https://gist.github.com/couchdeveloper/6155227
downloadImages is a button and whenever I press on it, a spinner should start rolling, an async request should ping Google (to make sure there is a connection) and after a response is received, I start to synchronically downloading images.
Somehow the spinner won't go and it seems as if the request is sync and not async.
- (IBAction)downloadImages:(id)sender {
NSString *ping=#"http://www.google.com/";
GlobalVars *globals = [GlobalVars sharedInstance];
[self startSpinner:#"Please Wait."];
NSURL *url = [[NSURL alloc] initWithString:ping];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data) {
for(int i=globals.farmerList.count-1; i>=0;i--)
{
//Definitions
NSString * documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//Get Image From URL
NSString *urlString = [NSString stringWithFormat:#"https://myurl.com/%#",[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"]];
UIImage * imageFromURL = [self getImageFromURL:urlString];
//Save Image to Directory
[self saveImage:imageFromURL withFileName:[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"] ofType:#"jpg" inDirectory:documentsDirectoryPath];
}
[self stopSpinner];
}
}];
}
The spinner code:
//show loading activity.
- (void)startSpinner:(NSString *)message {
// Purchasing Spinner.
if (!connectingAlerts) {
connectingAlerts = [[UIAlertView alloc] initWithTitle:NSLocalizedString(message,#"")
message:nil
delegate:self
cancelButtonTitle:nil
otherButtonTitles:nil];
connectingAlerts.tag = (NSUInteger)300;
[connectingAlerts show];
UIActivityIndicatorView *connectingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
connectingIndicator.frame = CGRectMake(139.0f-18.0f,50.0f,37.0f,37.0f);
[connectingAlerts addSubview:connectingIndicator];
[connectingIndicator startAnimating];
}
}
//hide loading activity.
- (void)stopSpinner {
if (connectingAlerts) {
[connectingAlerts dismissWithClickedButtonIndex:0 animated:YES];
connectingAlerts = nil;
}
// [self performSelector:#selector(showBadNews:) withObject:error afterDelay:0.1];
}
As asked: the getImageFromURL code
-(UIImage *) getImageFromURL:(NSString *)fileURL {
UIImage * result;
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]];
result = [UIImage imageWithData:data];
return result;
}
-(void) saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath {
if ([[extension lowercaseString] isEqualToString:#"png"]) {
[UIImagePNGRepresentation(image) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.%#", imageName, #"png"]] options:NSAtomicWrite error:nil];
} else if ([[extension lowercaseString] isEqualToString:#"jpg"] || [[extension lowercaseString] isEqualToString:#"jpeg"]) {
[UIImageJPEGRepresentation(image, 1.0) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.%#", imageName, #"jpg"]] options:NSAtomicWrite error:nil];
} else {
NSLog(#"Image Save Failed\nExtension: (%#) is not recognized, use (PNG/JPG)", extension);
}
}
That's because you're creating an asynchronous operation and then telling it to execute on the main thread by using [NSOperationQueue mainQueue];.
Instead, create a new instance of NSOpeartionQueue and pass that as the parameter.
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
This is an asynchronous problem. Asynchronism is infectious. That means, if any small part of the problem is asynchronous, the whole problem becomes asynchronous.
That is, your button action would invoke an asynchronous method like this (and itself becomes "asynchronous" as well):
- (IBAction)downloadImages:(id)sender
{
self.downloadImagesButton.enabled = NO;
[self asyncLoadAndSaveImagesWithURLs:self.urls completion:^(id result, NSError* error){
if (error != nil) {
NSLog(#"Error: %#", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
self.downloadImagesButton.enabled = YES;
};
}];
}
So, your asynchronous problem can be described as this:
Given a list of URLs, asynchronously load each URL and asynchronously save them to disk. When all URLs are loaded and saved, asynchronously notify the call-site by calling a completion handler passing it an array of results (for each download and save operation).
This is your asynchronous method:
typedef void (^completion_t)(id result, NSError* error);
- (void) asyncLoadAndSaveImagesWithURLs:(NSArray*)urls
completion:(completion_t) completionHandler;
Asynchronous problems can be solved properly only by finding a suitable asynchronous pattern. This involves to asynchronize every part of the problem.
Lets start with your getImageFromURL method. Loading a remote resource is inherently asynchronous, so your wrapper method ultimately will be asynchronous as well:
typedef void (^completion_t)(id result, NSError* error);
- (void) loadImageWithURL:(NSURL*)url completion:(completion_t)completionHandler;
I leave it undefined how that method will be eventually implemented. You may use NSURLConnection's asynchronous convenient class method, a third party helper tool or your own HTTPRequestOperation class. It doesn't matter but it MUST be asynchronous for achieving a sane approach.
Purposefully, you can and should make your saveImage method asynchronous as well. The reason for making this asynchronous is, that this method possibly will get invoked concurrently, and we should *serialize* disk bound (I/O bound) tasks. This improves utilization of system resources and also makes your approach a friendly system citizen.
Here is the asynchronized version:
typedef void (^completion_t)(id result, NSError* error);
-(void) saveImage:(UIImage *)image fileName:(NSString *)fileName ofType:(NSString *)extension
inDirectory:(NSString *)directoryPath
completion:(completion_t)completionHandler;
In order to serialize disk access, we can use a dedicated queue disk_queue where we assume it has been properly initialized as a serial queue by self:
-(void) saveImage:(UIImage *)image fileName:(NSString *)fileName ofType:(NSString *)extension
inDirectory:(NSString *)directoryPath
completion:(completion_t)completionHandler
{
dispatch_async(self.disk_queue, ^{
// save the image
...
if (completionHandler) {
completionHandler(result, nil);
}
});
}
Now, we can define an asynchronous wrapper which loads and saves the image:
typedef void (^completion_t)(id result, NSError* error);
- (void) loadAndSaveImageWithURL:(NSURL*)url completion:(completion_t)completionHandler
{
[self loadImageWithURL:url completion:^(id image, NSError*error) {
if (image) {
[self saveImage:image fileName:fileName ofType:type inDirectory:directory completion:^(id result, NSError* error){
if (result) {
if (completionHandler) {
completionHandler(result, nil);
}
}
else {
DebugLog(#"Error: %#", error);
if (completionHandler) {
completionHandler(nil, error);
}
}
}];
}
else {
if (completionHandler) {
completionHandler(nil, error);
}
}
}];
}
This loadAndSaveImageWithURL method actually performs a "continuation" of two asynchronous tasks:
First, asynchronously load the image.
THEN, if that was successful, asynchronously save the image.
It's important to notice that these two asynchronous tasks are sequentially processed.
Up until here, this all should be quite comprehensive and be straight forward. The tricky part follows now where we try to invoke a number of asynchronous tasks in an asynchronous manner.
Asynchronous Loop
Suppose, we have a list of URLs. Each URL shall be loaded asynchronously, and when all URLs are loaded we want the call-site to be notified.
The traditional for loop is not that appropriate for accomplishing this. But imagine we would have a Category for a NSArray with a method like this:
Category for NSArray
- (void) forEachApplyTask:(task_t)transform completion:(completion_t)completionHandler;
This basically reads: for each object in the array, apply the asynchronous task transform and when all objects have been "transformed" return a list of the transformed objects.
Note: this method is asynchronous!
With the appropriate "transform" function, we can "translate" this to your specific problem:
For each URL in the array, apply the asynchronous task loadAndSaveImageWithURL and when all URLS have been loaded and saved return a list of the results.
The actual implementation of the forEachApplyTask:completion: may appear a bit tricky and for brevity I don't want to post the complete source here. A viable approach requires about 40 lines of code.
I'll provide an example implementation later (on Gist), but lets explain how this method can be used:
The task_t is a "block" which takes one input parameter (the URL) and returns a result.
Since everything must be treated asynchronously, this block is asynchronous as well, and the eventual result will be provided via a completion block:
typedef void (^completion_t)(id result, NSError* error);
typedef void (^task_t)(id input, completion_t completionHandler);
The completion handler may be defined as follows:
If the tasks succeeds, parameter error equals nil. Otherwise, parameter error is an NSError object. That is, a valid result may also be nil.
We can quite easily wrap our method loadAndSaveImageWithURL:completion: and create a block:
task_t task = ^(id input, completion_t completionHandler) {
[self loadAndSaveImageWithURL:input completion:completionHandler];
};
Given an array of URLs:
self.urls = ...;
your button action can be implemented as follows:
- (IBAction)downloadImages:(id)sender
{
self.downloadImagesButton.enabled = NO;
task_t task = ^(id input, completion_t completionHandler) {
[self loadAndSaveImageWithURL:input completion:completionHandler];
};
[self.urls forEachApplyTask:task ^(id results, NSError*error){
self.downloadImagesButton.enabled = YES;
if (error == nil) {
... // do something
}
else {
// handle error
}
}];
}
Again, notice that method forEachApplyTask:completion: is an asynchronous method, which returns immediately. The call-site will be notified via the completion handler.
The downloadImages method is asynchronous as well, there is no completion handler though. This method disables the button when it starts and enables it again when the asynchronous operation has been completed.
The implementation of this forEachApplyTask method can be found here: (https://gist.github.com/couchdeveloper/6155227).
From your code what I can understand is its not due to assyncronous call to load url. but the following code may heavy.
For assynchronous image loading try https://github.com/rs/SDWebImage
//Get Image From URL
NSString *urlString = [NSString stringWithFormat:#"https://myurl.com/%#",[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"]];
UIImage * imageFromURL = [self getImageFromURL:urlString];
//Save Image to Directory
[self saveImage:imageFromURL withFileName:[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"] ofType:#"jpg" inDirectory:documentsDirectoryPath];
Happy coding :)
NSString *pictureUrl = [[[oneUserDict objectForKey:#"picture"]objectForKey:#"data"]objectForKey:#"url"];
[[AppEngine sharedEngine]imageAtURL:[NSURL URLWithString:pictureUrl] onCompletion:^(UIImage *fetchedImage, NSURL *url, BOOL isInCache)
{
int index = [usersArray indexOfObject:oneUserDict];
NSString *loadName = [NSString stringWithFormat:#"%d of %d",index,[usersArray count]];
NSLog(#"%i",usersArray.count);
int temp=[usersArray count]-10;
if (index!=temp)
{
[[LoadingIndicator currentIndicator]displayActivity:loadName];
NSLog(#"inside loading indicator");
}
else
{
[[LoadingIndicator currentIndicator]displayCompleted:#"Done"];
NSLog(#"finally done");
}
aPerson.image = UIImagePNGRepresentation(fetchedImage);
[appDelegate.managedObjectContext save:nil];
}];
AppEngine is the subclass of MKNetworkEngine which uses a method called imageAtURL:onCompletion:
what I am currently doing is retrieving all the images from a particular url and and storing them in aPerson.image,basically the above code is in a FOR loop(i.e the for the count of users).
Issues
The above code which is in the completion block never gets executed,i dont know why but i have put a breakpoint inside the block but still the compiler wont run the statements inside the completion block.
Api imageAtURL:onCompletion: is deprecated. Use imageAtURL:completionHandler:errorHandler: instead. Also MKNetworkKit provides for UIImageView+MKNetworkKitAdditions category which provides simple API for image download like setImageFromURL: placeHolderImage:
Cheers!
Amar.