Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

sendAsynchronousRequest makes UI freezes

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);
    }
}
like image 740
izzy Avatar asked Nov 17 '13 13:11

izzy


2 Answers

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];
like image 140
Mick MacCallum Avatar answered Nov 03 '22 16:11

Mick MacCallum


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).

like image 27
CouchDeveloper Avatar answered Nov 03 '22 14:11

CouchDeveloper