sendAsynchronousRequest hace que la interfaz de usuario se congele

downloadImages es un button y cada vez que lo presiono, un spinner debería comenzar a girar, una request asíncrona debería hacer ping a Google (para asegurarme de que hay una connection) y después de recibir una respuesta, empiezo a download imágenes sincrónicamente.

De alguna manera, la ruleta no irá y parece que la request se sincroniza y no es asincrónica.

- (IBAction)downloadImages:(id)sender { NSString *ping=@"http://www.google.com/"; GlobalVars *globals = [GlobalVars shanetworkingInstance]; [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]; } }]; } 

El código spinner:

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

Como se le preguntó: el código getImageFromURL

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

Este es un problema asynchronous. El asincronismo es infeccioso . Eso significa que, si una pequeña parte del problema es asíncrona, todo el problema se vuelve asíncrono.

Es decir, la acción del button invocaría un método asíncrono como este (y también se vuelve "asíncrono"):

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

Entonces, su problema asynchronous se puede describir así:

Dado una list de URL, cargue asíncronamente cada URL y guárdelas de forma asíncrona en el disco. Cuando todas las URL se cargan y se guardan, notifican de forma asíncrona al sitio de llamadas llamando a un manejador de finalización que le pasa una matriz de resultados (para cada operación de descarga y guardado).

Este es tu método asynchronous:

 typedef void (^completion_t)(id result, NSError* error); - (void) asyncLoadAndSaveImagesWithURLs:(NSArray*)urls completion:(completion_t) completionHandler; 

Los problemas asíncronos pueden resolverse correctamente solo al encontrar un patrón asíncrono adecuado. Esto implica asincronizar cada parte del problema.

Comencemos con su método getImageFromURL . La carga de un recurso remoto es inherentemente asíncrona, por lo que su método de envoltorio también será asíncrono :

 typedef void (^completion_t)(id result, NSError* error); - (void) loadImageWithURL:(NSURL*)url completion:(completion_t)completionHandler; 

Dejo sin definir cómo se implementará finalmente ese método. Puede utilizar el método de class conveniente asíncrono de NSURLConnection, una herramienta auxiliar de terceros o su propia class HTTPRequestOperation . No importa, pero DEBE ser asíncrono para lograr un enfoque sano .

A propósito, puede y debe hacer su método saveImage asíncrono también. La razón para hacer esto asíncrono es que este método posiblemente se invocará al mismo time , y deberíamos * serializar * tareas enlazadas al disco (vinculadas de E / S). Esto mejora la utilización de los resources del sistema y también hace que su enfoque sea un ciudadano amigable.

Aquí está la versión asincronizada:

 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; 

Para serializar el acceso al disco, podemos usar una queue dedicada disk_queue donde suponemos que se ha inicializado correctamente como una queue de serie por 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); } }); } 

Ahora, podemos definir un contenedor asíncrono que carga y guarda la 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); } } }]; } 

Este método loadAndSaveImageWithURL realmente realiza una "continuación" de dos tareas asíncronas:

Primero, cargue asincrónicamente la image. ENTONCES, si eso fue exitoso, guarde la image asincrónicamente.

Es importante notar que estas dos tareas asíncronas se procesan secuencialmente .

Hasta aquí, todo esto debería ser bastante completo y ser sencillo. La parte complicada sigue ahora donde intentamos invocar varias tareas asincrónicas de forma asíncrona.

Bucle asíncrono

Supongamos que tenemos una list de URL. Cada URL se cargará de forma asíncrona y, cuando se carguen todas las URL, queremos que se notifique el sitio de la llamada.

El tradicional for bucle no es apropiado para lograr esto. Pero imagina que tendríamos una Categoría para un NSArray con un método como este:

Categoría para NSArray

 - (void) forEachApplyTask:(task_t)transform completion:(completion_t)completionHandler; 

Esto básicamente dice: para cada object en la matriz, aplique la transform tarea asíncrona y cuando todos los objects hayan sido "transformados" devuelva una list de los objects transformados.

Nota: ¡este método es asíncrono!

Con la function "transformar" apropiada, podemos "traducir" esto a su problema específico:

Para cada URL en la matriz, aplique la tarea asincrónica loadAndSaveImageWithURL y cuando todas las URL se hayan cargado y guardado, devuelva una list de los resultados.

La implementación real de forEachApplyTask:completion: puede parecer un poco complicado y, por razones de brevedad, no deseo publicar la fuente completa aquí. Un enfoque viable requiere cerca de 40 líneas de código.

Proporcionaré un ejemplo de implementación más adelante (en Gist), pero explico cómo se puede usar este método:

El task_t es un "bloque" que toma un parámetro de input (la URL) y devuelve un resultado. Dado que todo debe tratarse de forma asíncrona, este bloque también es asíncrono , y el resultado final se proporcionará mediante un bloque de finalización:

 typedef void (^completion_t)(id result, NSError* error); typedef void (^task_t)(id input, completion_t completionHandler); 

El manejador de finalización puede definirse de la siguiente manera:

Si las tareas se realizan correctamente, el error de parámetro es igual a nil . De lo contrario, el error de parámetro es un object NSError . Es decir, un resultado válido también puede ser nil .

Podemos ajustar fácilmente nuestro método loadAndSaveImageWithURL:completion: y crear un bloque:

 task_t task = ^(id input, completion_t completionHandler) { [self loadAndSaveImageWithURL:input completion:completionHandler]; }; 

Dado un set de URL:

 self.urls = ...; 

su acción de button se puede implementar de la siguiente manera:

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

Nuevamente, observe que el método forEachApplyTask:completion: es un método asíncrono , que regresa inmediatamente. El sitio de llamadas se notificará a través del manejador de finalización.

El método downloadImages es asíncrono, no hay un manejador de finalización. Este método desactiva el button cuando se inicia y lo habilita nuevamente cuando se ha completado la operación asíncrona.

La implementación de este método forEachApplyTask se puede encontrar aquí: ( https://gist.github.com/couchdeveloper/6155227 ).

Esto se debe a que está creando una operación asíncrona y luego diciéndole que se ejecute en el hilo principal utilizando [NSOperationQueue mainQueue]; .

En su lugar, cree una nueva instancia de NSOpeartionQueue y pase eso como el parámetro.

 NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; 

Desde su código, lo que puedo entender es que no se debe a una llamada asíncrona para cargar url. pero el siguiente código puede ser pesado.

Para la carga de imágenes asíncronas, intente 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]; 

Codificación feliz 🙂