iOS descargar y guardar la imagen dentro de la aplicación

78

¿Puedo descargar una imagen del sitio web y guardarla permanentemente dentro de mi aplicación? Realmente no tengo idea, pero sería una buena función para mi aplicación.

Samuli Lehtonen
fuente

Respuestas:

92

Si bien es cierto que las otras respuestas aquí funcionarán, realmente no son soluciones que deberían usarse en el código de producción . (al menos no sin modificaciones)

Problemas

El problema con estas respuestas es que si se implementan como están y no se llaman desde un hilo en segundo plano, bloquearán el hilo principal mientras descargan y guardan la imagen. Esto es malo .

Si el hilo principal está bloqueado, las actualizaciones de la interfaz de usuario no se realizarán hasta que se complete la descarga / guardado de la imagen. Como ejemplo de lo que esto significa, digamos que agrega un UIActivityIndicatorView a su aplicación para mostrarle al usuario que la descarga aún está en progreso (lo usaré como ejemplo a lo largo de esta respuesta) con el siguiente flujo de control aproximado:

  1. Se carga el objeto responsable de iniciar la descarga.
  2. Dile al indicador de actividad que comience a animar.
  3. Inicie el proceso de descarga sincrónica usando +[NSData dataWithContentsOfURL:]
  4. Guarde los datos (imagen) que acaba de descargar.
  5. Dile al indicador de actividad que deje de animar.

Ahora bien, esto puede parecer un flujo de control razonable, pero esconde un problema crítico.

Cuando llama al método startAnimating del indicador de actividad en el hilo principal (UI), las actualizaciones de UI para este evento no sucederán hasta la próxima vez que se actualice el ciclo de ejecución principal , y aquí es donde está el primer problema importante.

Antes de que se produzca esta actualización, se activa la descarga y, dado que se trata de una operación sincrónica, bloquea el hilo principal hasta que finaliza la descarga (guardar tiene el mismo problema). En realidad, esto evitará que el indicador de actividad comience su animación. Después de eso, llama al método stopAnimating del indicador de actividad y espera que todo sea bueno, pero no lo es.

En este punto, probablemente se estará preguntando lo siguiente.

¿Por qué nunca aparece mi indicador de actividad?

Bueno, piénsalo así. Le dice al indicador que comience, pero no tiene oportunidad antes de que comience la descarga. Una vez que se completa la descarga, le indica al indicador que deje de animarse. Dado que el subproceso principal se bloqueó durante toda la operación, el comportamiento que realmente ve es más parecido a decirle al indicador que se inicie y luego decirle inmediatamente que se detenga, a pesar de que hubo una (posiblemente) gran tarea de descarga en el medio.

Ahora, en el mejor de los casos , todo lo que esto hace es causar una mala experiencia de usuario (aún muy mala). Incluso si cree que esto no es un gran problema porque solo está descargando una imagen pequeña y la descarga ocurre casi instantáneamente, ese no siempre será el caso. Algunos de sus usuarios pueden tener conexiones a Internet lentas, o algo puede estar mal en el lado del servidor que impide que la descarga se inicie inmediatamente o en absoluto.

En ambos casos, la aplicación no podrá procesar las actualizaciones de la interfaz de usuario, ni siquiera tocar los eventos mientras su tarea de descarga se sienta alrededor haciendo girar sus pulgares esperando que se complete la descarga o que el servidor responda a su solicitud.

Lo que esto significa es que la descarga sincrónica desde el hilo principal le impide implementar algo para indicar al usuario que hay una descarga en curso. Y dado que los eventos táctiles también se procesan en el hilo principal, esto descarta la posibilidad de agregar cualquier tipo de botón de cancelación también.

Luego, en el peor de los casos , comenzará a recibir informes de fallas que indiquen lo siguiente.

Tipo de excepción: 00000020 Códigos de excepción: 0x8badf00d

Estos son fáciles de identificar por el código de excepción 0x8badf00d, que puede leerse como "comió comida en mal estado". Esta excepción es lanzada por el temporizador del perro guardián, cuyo trabajo es vigilar las tareas de ejecución prolongada que bloquean el hilo principal y eliminar la aplicación infractora si esto dura demasiado tiempo. Podría decirse que este sigue siendo un problema de mala experiencia del usuario, pero si esto comienza a ocurrir, la aplicación ha cruzado la línea entre la mala experiencia del usuario y la terrible experiencia del usuario.

Aquí hay más información sobre lo que puede causar que esto suceda en Preguntas y respuestas técnicas de Apple sobre redes síncronas (abreviado para abreviar).

La causa más común de bloqueos de tiempo de espera de vigilancia en una aplicación de red es la conexión en red síncrona en el hilo principal. Hay cuatro factores que contribuyen aquí:

  1. Redes síncronas: aquí es donde realiza una solicitud de red y bloquea la espera de la respuesta.
  2. hilo principal: la red síncrona es menos que ideal en general, pero causa problemas específicos si lo hace en el hilo principal. Recuerde que el hilo principal es responsable de ejecutar la interfaz de usuario. Si bloquea el hilo principal durante un período de tiempo significativo, la interfaz de usuario deja de responder de forma inaceptable.
  3. tiempos de espera prolongados: si la red simplemente desaparece (por ejemplo, el usuario está en un tren que entra en un túnel), cualquier solicitud de red pendiente no fallará hasta que haya expirado el tiempo de espera ...

...

  1. perro guardián: para mantener la interfaz de usuario receptiva, iOS incluye un mecanismo de perro guardián. Si su aplicación no responde a ciertos eventos de la interfaz de usuario (iniciar, suspender, reanudar, terminar) a tiempo, el perro guardián cerrará su aplicación y generará un informe de bloqueo del tiempo de espera del perro guardián. La cantidad de tiempo que le da el perro guardián no está documentada formalmente, pero siempre es menor que el tiempo de espera de la red.

Un aspecto complicado de este problema es que depende en gran medida del entorno de red. Si siempre prueba su aplicación en su oficina, donde la conectividad de red es buena, nunca verá este tipo de fallas. Sin embargo, una vez que comience a implementar su aplicación para los usuarios finales, quienes la ejecutarán en todo tipo de entornos de red, los bloqueos como este se volverán comunes.

Ahora, en este punto, dejaré de divagar sobre por qué las respuestas proporcionadas pueden ser problemáticas y comenzaré a ofrecer algunas soluciones alternativas. Tenga en cuenta que he usado la URL de una imagen pequeña en estos ejemplos y notará una diferencia mayor cuando use una imagen de mayor resolución.


Soluciones

Comenzaré mostrando una versión segura de las otras respuestas, con la adición de cómo manejar las actualizaciones de la interfaz de usuario. Este será el primero de varios ejemplos, todos los cuales supondrán que la clase en la que se implementan tiene propiedades válidas para un UIImageView, un UIActivityIndicatorView, así como el documentsDirectoryURLmétodo para acceder al directorio de documentos. En el código de producción, es posible que desee implementar su propio método para acceder al directorio de documentos como una categoría en NSURL para una mejor reutilización del código, pero para estos ejemplos, esto estará bien.

- (NSURL *)documentsDirectoryURL
{
    NSError *error = nil;
    NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory
                                                        inDomain:NSUserDomainMask
                                               appropriateForURL:nil
                                                          create:NO
                                                           error:&error];
    if (error) {
        // Figure out what went wrong and handle the error.
    }
    
    return url;
}

Estos ejemplos también supondrán que el hilo en el que comienzan es el hilo principal. Es probable que este sea el comportamiento predeterminado a menos que inicie la tarea de descarga desde algún lugar como el bloque de devolución de llamada de alguna otra tarea asincrónica. Si inicia la descarga en un lugar típico, como un método de ciclo de vida de un controlador de vista (es decir, viewDidLoad, viewWillAppear :, etc.), esto producirá el comportamiento esperado.

Este primer ejemplo utilizará el +[NSData dataWithContentsOfURL:]método, pero con algunas diferencias clave. Por un lado, notará que en este ejemplo, la primera llamada que hacemos es decirle al indicador de actividad que comience a animar, entonces hay una diferencia inmediata entre esto y los ejemplos sincrónicos. Inmediatamente, usamos dispatch_async (), pasando la cola concurrente global para mover la ejecución al hilo de fondo.

En este punto, ya ha mejorado mucho su tarea de descarga. Dado que todo dentro del bloque dispatch_async () ahora saldrá del hilo principal, su interfaz ya no se bloqueará y su aplicación será libre de responder a eventos táctiles.

Lo que es importante notar aquí es que todo el código dentro de este bloque se ejecutará en el hilo de fondo, hasta el punto en el que la descarga / guardado de la imagen fue exitosa, momento en el que es posible que desee decirle al indicador de actividad que se detenga. , o aplique la imagen recién guardada a un UIImageView. De cualquier manera, estas son actualizaciones de la interfaz de usuario, lo que significa que debe enviar el hilo principal usando dispatch_get_main_queue () para realizarlas. No hacerlo da como resultado un comportamiento indefinido, lo que puede hacer que la interfaz de usuario se actualice después de un período de tiempo inesperado o incluso puede provocar un bloqueo. Asegúrese siempre de volver al hilo principal antes de realizar actualizaciones de la interfaz de usuario.

// Start the activity indicator before moving off the main thread
[self.activityIndicator startAnimating];
// Move off the main thread to start our blocking tasks.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Create the image URL from a known string.
    NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
    
    NSError *downloadError = nil;
    // Create an NSData object from the contents of the given URL.
    NSData *imageData = [NSData dataWithContentsOfURL:imageURL
                                              options:kNilOptions
                                                error:&downloadError];
    // ALWAYS utilize the error parameter!
    if (downloadError) {
        // Something went wrong downloading the image. Figure out what went wrong and handle the error.
        // Don't forget to return to the main thread if you plan on doing UI updates here as well.
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.activityIndicator stopAnimating];
            NSLog(@"%@",[downloadError localizedDescription]);
        });
    } else {
        // Get the path of the application's documents directory.
        NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
        // Append the desired file name to the documents directory path.
        NSURL *saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"GCD.png"];

        NSError *saveError = nil;
        BOOL writeWasSuccessful = [imageData writeToURL:saveLocation
                                                options:kNilOptions
                                                  error:&saveError];
        // Successful or not we need to stop the activity indicator, so switch back the the main thread.
        dispatch_async(dispatch_get_main_queue(), ^{
            // Now that we're back on the main thread, you can make changes to the UI.
            // This is where you might display the saved image in some image view, or
            // stop the activity indicator.
            
            // Check if saving the file was successful, once again, utilizing the error parameter.
            if (writeWasSuccessful) {
                // Get the saved image data from the file.
                NSData *imageData = [NSData dataWithContentsOfURL:saveLocation];
                // Set the imageView's image to the image we just saved.
                self.imageView.image = [UIImage imageWithData:imageData];
            } else {
                NSLog(@"%@",[saveError localizedDescription]);
                // Something went wrong saving the file. Figure out what went wrong and handle the error.
            }
            
            [self.activityIndicator stopAnimating];
        });
    }
});

Ahora tenga en cuenta que el método que se muestra arriba todavía no es una solución ideal considerando que no se puede cancelar antes de tiempo, no le da ninguna indicación del progreso de la descarga, no puede manejar ningún tipo de desafío de autenticación, puede No se le dará un intervalo de tiempo de espera específico, etc. (muchas, muchas razones). Cubriré algunas de las mejores opciones a continuación.

En estos ejemplos, solo cubriré soluciones para aplicaciones dirigidas a iOS 7 en adelante, considerando (en el momento de escribir este artículo) que iOS 8 es la versión principal actual, y Apple sugiere que solo admitan las versiones N y N-1 . Si necesita admitir versiones anteriores de iOS, le recomiendo buscar en la clase NSURLConnection , así como en la versión 1.0 de AFNetworking. Si nos fijamos en el historial de revisión de esta respuesta, se pueden encontrar ejemplos básicos utilizando NSURLConnection y ASIHTTPRequest , aunque debe tenerse en cuenta que ASIHTTPRequest ya no se mantiene, y debe no ser utilizado para nuevos proyectos.


NSURLSession

Comencemos con NSURLSession , que se introdujo en iOS 7 y mejora en gran medida la facilidad con la que se pueden realizar redes en iOS. Con NSURLSession, puede realizar fácilmente solicitudes HTTP asincrónicas con un bloque de devolución de llamada y manejar los desafíos de autenticación con su delegado. Pero lo que hace que esta clase sea realmente especial es que también permite que las tareas de descarga continúen ejecutándose incluso si la aplicación se envía a un segundo plano, se termina o incluso se bloquea. Aquí hay un ejemplo básico de su uso.

// Start the activity indicator before starting the download task.
[self.activityIndicator startAnimating];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Use a session with a custom configuration
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
// Create the image URL from some known string.
NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
// Create the download task passing in the URL of the image.
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:imageURL completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
    // Get information about the response if neccessary.
    if (error) {
        NSLog(@"%@",[error localizedDescription]);
        // Something went wrong downloading the image. Figure out what went wrong and handle the error.
        // Don't forget to return to the main thread if you plan on doing UI updates here as well.
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.activityIndicator stopAnimating];
        });
    } else {
        NSError *openDataError = nil;
        NSData *downloadedData = [NSData dataWithContentsOfURL:location
                                                       options:kNilOptions
                                                         error:&openDataError];
        if (openDataError) {
            // Something went wrong opening the downloaded data. Figure out what went wrong and handle the error.
            // Don't forget to return to the main thread if you plan on doing UI updates here as well.
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"%@",[openDataError localizedDescription]);
                [self.activityIndicator stopAnimating];
            });
        } else {
            // Get the path of the application's documents directory.
            NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
            // Append the desired file name to the documents directory path.
            NSURL *saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"NSURLSession.png"];
            NSError *saveError = nil;
            
            BOOL writeWasSuccessful = [downloadedData writeToURL:saveLocation
                                                          options:kNilOptions
                                                            error:&saveError];
            // Successful or not we need to stop the activity indicator, so switch back the the main thread.
            dispatch_async(dispatch_get_main_queue(), ^{
                // Now that we're back on the main thread, you can make changes to the UI.
                // This is where you might display the saved image in some image view, or
                // stop the activity indicator.
                
                // Check if saving the file was successful, once again, utilizing the error parameter.
                if (writeWasSuccessful) {
                    // Get the saved image data from the file.
                    NSData *imageData = [NSData dataWithContentsOfURL:saveLocation];
                    // Set the imageView's image to the image we just saved.
                    self.imageView.image = [UIImage imageWithData:imageData];
                } else {
                    NSLog(@"%@",[saveError localizedDescription]);
                    // Something went wrong saving the file. Figure out what went wrong and handle the error.
                }
                
                [self.activityIndicator stopAnimating];
            });
        }
    }
}];

// Tell the download task to resume (start).
[task resume];

A partir de esto, notará que el downloadTaskWithURL: completionHandler:método devuelve una instancia de NSURLSessionDownloadTask, en la que -[NSURLSessionTask resume]se llama a un método de instancia . Este es el método que realmente le dice a la tarea de descarga que comience. Esto significa que puede acelerar su tarea de descarga y, si lo desea, dejar de iniciarla (si es necesario). Esto también significa que siempre que almacene una referencia a la tarea, también puede utilizar sus métodos cancely suspendpara cancelar o pausar la tarea si es necesario.

Lo realmente genial de NSURLSessionTasks es que con un poco de KVO , puede monitorear los valores de sus propiedades countOfBytesExpectedToReceive y countOfBytesReceived, alimentar estos valores a un NSByteCountFormatter y crear fácilmente un indicador de progreso de descarga para su usuario con unidades legibles por humanos (por ejemplo, 42 KB de 100 KB).

Sin embargo, antes de alejarme de NSURLSession, me gustaría señalar que se puede evitar la fealdad de tener que dispatch_async de regreso a los hilos principales en varios puntos diferentes en el bloque de devolución de llamada de la descarga. Si elige seguir esta ruta, puede inicializar la sesión con su inicializador que le permite especificar el delegado, así como la cola de delegados. Esto requerirá que use el patrón de delegado en lugar de los bloques de devolución de llamada, pero esto puede ser beneficioso porque es la única forma de admitir descargas en segundo plano.

NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
                                                      delegate:self
                                                 delegateQueue:[NSOperationQueue mainQueue]];

AFNetworking 2.0

Si nunca ha oído hablar de AFNetworking , en mi humilde opinión es el fin de todas las bibliotecas de redes. Fue creado para Objective-C, pero también funciona en Swift. En palabras de su autor:

AFNetworking es una biblioteca de redes deliciosa para iOS y Mac OS X. Está construida sobre el sistema de carga de URL Foundation, extendiendo las poderosas abstracciones de redes de alto nivel integradas en Cocoa. Tiene una arquitectura modular con API bien diseñadas y ricas en funciones que son un placer de usar.

AFNetworking 2.0 es compatible con iOS 6 y versiones posteriores, pero en este ejemplo, usaré su clase AFHTTPSessionManager, que requiere iOS 7 y versiones posteriores debido al uso de todas las API nuevas en la clase NSURLSession. Esto resultará obvio cuando lea el siguiente ejemplo, que comparte una gran cantidad de código con el ejemplo NSURLSession anterior.

Sin embargo, hay algunas diferencias que me gustaría señalar. Para empezar, en lugar de crear su propia NSURLSession, creará una instancia de AFURLSessionManager, que administrará internamente una NSURLSession. Hacerlo le permite aprovechar algunos de sus métodos de conveniencia como -[AFURLSessionManager downloadTaskWithRequest:progress:destination:completionHandler:]. Lo interesante de este método es que le permite crear de manera bastante concisa una tarea de descarga con una ruta de archivo de destino determinada, un bloque de finalización y una entrada para un puntero NSProgress , en el que puede observar información sobre el progreso de la descarga. He aquí un ejemplo.

// Use the default session configuration for the manager (background downloads must use the delegate APIs)
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Use AFNetworking's NSURLSessionManager to manage a NSURLSession.
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

// Create the image URL from some known string.
NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
// Create a request object for the given URL.
NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
// Create a pointer for a NSProgress object to be used to determining download progress.
NSProgress *progress = nil;

// Create the callback block responsible for determining the location to save the downloaded file to.
NSURL *(^destinationBlock)(NSURL *targetPath, NSURLResponse *response) = ^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    // Get the path of the application's documents directory.
    NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
    NSURL *saveLocation = nil;
    
    // Check if the response contains a suggested file name
    if (response.suggestedFilename) {
        // Append the suggested file name to the documents directory path.
        saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:response.suggestedFilename];
    } else {
        // Append the desired file name to the documents directory path.
        saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"AFNetworking.png"];
    }

    return saveLocation;
};

// Create the completion block that will be called when the image is done downloading/saving.
void (^completionBlock)(NSURLResponse *response, NSURL *filePath, NSError *error) = ^void (NSURLResponse *response, NSURL *filePath, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // There is no longer any reason to observe progress, the download has finished or cancelled.
        [progress removeObserver:self
                      forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
        
        if (error) {
            NSLog(@"%@",error.localizedDescription);
            // Something went wrong downloading or saving the file. Figure out what went wrong and handle the error.
        } else {
            // Get the data for the image we just saved.
            NSData *imageData = [NSData dataWithContentsOfURL:filePath];
            // Get a UIImage object from the image data.
            self.imageView.image = [UIImage imageWithData:imageData];
        }
    });
};

// Create the download task for the image.
NSURLSessionDownloadTask *task = [manager downloadTaskWithRequest:request
                                                         progress:&progress
                                                      destination:destinationBlock
                                                completionHandler:completionBlock];
// Start the download task.
[task resume];

// Begin observing changes to the download task's progress to display to the user.
[progress addObserver:self
           forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
              options:NSKeyValueObservingOptionNew
              context:NULL];

Por supuesto, dado que hemos agregado la clase que contiene este código como un observador a una de las propiedades de la instancia de NSProgress, tendrá que implementar el -[NSObject observeValueForKeyPath:ofObject:change:context:]método. En este caso, he incluido un ejemplo de cómo puede actualizar una etiqueta de progreso para mostrar el progreso de la descarga. Es muy facil. NSProgress tiene un método de instancia localizedDescriptionque mostrará información de progreso en un formato legible para humanos localizado.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    // We only care about updates to fractionCompleted
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(fractionCompleted))]) {
        NSProgress *progress = (NSProgress *)object;
        // localizedDescription gives a string appropriate for display to the user, i.e. "42% completed"
        self.progressLabel.text = progress.localizedDescription;
    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

No olvide, si desea utilizar AFNetworking en su proyecto, deberá seguir sus instrucciones de instalación y asegurarse de hacerlo #import <AFNetworking/AFNetworking.h>.

Alamofire

Y finalmente, me gustaría dar un ejemplo final usando Alamofire . Esta es una biblioteca que hace que la creación de redes en Swift sea muy fácil. No tengo personajes para entrar en gran detalle sobre el contenido de esta muestra, pero hace más o menos lo mismo que los últimos ejemplos, solo que de una manera posiblemente más hermosa.

// Create the destination closure to pass to the download request. I haven't done anything with them
// here but you can utilize the parameters to make adjustments to the file name if neccessary.
let destination = { (url: NSURL!, response: NSHTTPURLResponse!) -> NSURL in
    var error: NSError?
    // Get the documents directory
    let documentsDirectory = NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory,
        inDomain: .UserDomainMask,
        appropriateForURL: nil,
        create: false,
        error: &error
    )
    
    if let error = error {
        // This could be bad. Make sure you have a backup plan for where to save the image.
        println("\(error.localizedDescription)")
    }
    
    // Return a destination of .../Documents/Alamofire.png
    return documentsDirectory!.URLByAppendingPathComponent("Alamofire.png")
}

Alamofire.download(.GET, "http://www.google.com/images/srpr/logo3w.png", destination)
    .validate(statusCode: 200..<299) // Require the HTTP status code to be in the Successful range.
    .validate(contentType: ["image/png"]) // Require the content type to be image/png.
    .progress { (bytesRead, totalBytesRead, totalBytesExpectedToRead) in
        // Create an NSProgress object to represent the progress of the download for the user.
        let progress = NSProgress(totalUnitCount: totalBytesExpectedToRead)
        progress.completedUnitCount = totalBytesRead
        
        dispatch_async(dispatch_get_main_queue()) {
            // Move back to the main thread and update some progress label to show the user the download is in progress.
            self.progressLabel.text = progress.localizedDescription
        }
    }
    .response { (request, response, _, error) in
        if error != nil {
            // Something went wrong. Handle the error.
        } else {
            // Open the newly saved image data.
            if let imageData = NSData(contentsOfURL: destination(nil, nil)) {
                dispatch_async(dispatch_get_main_queue()) {
                    // Move back to the main thread and add the image to your image view.
                    self.imageView.image = UIImage(data: imageData)
                }
            }
        }
    }
Mick MacCallum
fuente
¿Cómo recomiendas que obtengamos DocumentsDirectoryUrl para la solución AFNetworking?
SleepsOnNewspapers
2
@HomelessPeopleCanCode Cerca de la parte superior de mi respuesta debajo del encabezado "Soluciones" incluí ese método y lo usé en todos mis ejemplos de Objective-C, sin embargo, hay más opciones disponibles . La mayoría de estos le proporcionarán la ruta al directorio de documentos en forma de NSString, por lo que deberá convertirlo a NSURL para poder usarlos con mis ejemplos sin tener que modificarlos, lo que se puede hacer como esto: NSURL *filePathURL = [NSURL fileURLWithPath:filePathString];.
Mick MacCallum
bien explicad. ¿Cómo guardar en Fotos? vía Alamofire. ¿Qué pasar como parámetro en 'destino'?
khunshan
¡Guauu! Brillante respuesta, muy informativa. ¡Gracias!
Koushik Ravikumar
Una de las mejores respuestas que he leído en este sitio. Muy útil e informativo. Gracias por tomarse el tiempo para educarnos plebls;)
Alexandre G.
39

No puede guardar nada dentro del paquete de la aplicación, pero puede usarlo +[NSData dataWithContentsOfURL:]para almacenar la imagen en el directorio de documentos de su aplicación, por ejemplo:

NSData *imageData = [NSData dataWithContentsOfURL:myImageURL];
NSString *imagePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"/myImage.png"];
[imageData writeToFile:imagePath atomically:YES];

No es exactamente permanente , pero permanece ahí al menos hasta que el usuario borra la aplicación.


fuente
3
Esta respuesta es mejor que la aceptada, porque si la guarda como PNG o JPEG usando UIImage UIImageJPEGRepresentation o UIImagePNGRepresentation, el tamaño de los datos en el disco del iPhone es el doble que el original. Con este código, simplemente almacena los datos originales.
jcesarmobile
13

Ese es el concepto principal. Que te diviertas ;)

NSURL *url = [NSURL URLWithString:@"http://example.com/yourImage.png"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
path = [path stringByAppendingString:@"/yourLocalImage.png"];
[data writeToFile:path atomically:YES];
cem
fuente
7

Dado que ahora estamos en IO5, ya no es necesario escribir imágenes en el disco.
Ahora puede establecer "permitir almacenamiento externo" en un atributo binario de coredata. Según las notas de publicación de las manzanas, significa lo siguiente:

Los valores de datos pequeños, como las miniaturas de imágenes, se pueden almacenar de manera eficiente en una base de datos, pero las fotos grandes u otros medios se manejan mejor directamente mediante el sistema de archivos. Ahora puede especificar que el valor de un atributo de un objeto administrado puede almacenarse como un registro externo; consulte setAllowsExternalBinaryDataStorage: cuando está habilitado, Core Data decide heurísticamente por valor si debe guardar los datos directamente en la base de datos o almacenar un URI a un archivo separado que administra por usted. No puede realizar consultas basadas en el contenido de una propiedad de datos binarios si usa esta opción.

Alejandro
fuente
3

Como dijeron otras personas, hay muchos casos en los que debe descargar una imagen en el hilo de fondo sin bloquear la interfaz de usuario

En estos casos, mi solución favorita es usar un método conveniente con bloques, como este: (crédito -> iOS: Cómo descargar imágenes de forma asincrónica (y hacer que su UITableView se desplace rápidamente) )

- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                               if ( !error )
                               {
                                   UIImage *image = [[UIImage alloc] initWithData:data];
                                   completionBlock(YES,image);
                               } else{
                                   completionBlock(NO,nil);
                               }
                           }];
}

Y llámalo como

NSURL *imageUrl = //...

[[MyUtilManager sharedInstance] downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
    //Here you can save the image permanently, update UI and do what you want...
}];
andreacipriani
fuente
1

Así es como descargo un banner publicitario. Es mejor hacerlo en segundo plano si está descargando una imagen grande o un montón de imágenes.

- (void)viewDidLoad {
    [super viewDidLoad];

    [self performSelectorInBackground:@selector(loadImageIntoMemory) withObject:nil];

}
- (void)loadImageIntoMemory {
    NSString *temp_Image_String = [[NSString alloc] initWithFormat:@"http://yourwebsite.com/MyImageName.jpg"];
    NSURL *url_For_Ad_Image = [[NSURL alloc] initWithString:temp_Image_String];
    NSData *data_For_Ad_Image = [[NSData alloc] initWithContentsOfURL:url_For_Ad_Image];
    UIImage *temp_Ad_Image = [[UIImage alloc] initWithData:data_For_Ad_Image];
    [self saveImage:temp_Ad_Image];
    UIImageView *imageViewForAdImages = [[UIImageView alloc] init];
    imageViewForAdImages.frame = CGRectMake(0, 0, 320, 50);
    imageViewForAdImages.image = [self loadImage];
    [self.view addSubview:imageViewForAdImages];
}
- (void)saveImage: (UIImage*)image {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString* path = [documentsDirectory stringByAppendingPathComponent: @"MyImageName.jpg" ];
    NSData* data = UIImagePNGRepresentation(image);
    [data writeToFile:path atomically:YES];
}
- (UIImage*)loadImage {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString* path = [documentsDirectory stringByAppendingPathComponent:@"MyImageName.jpg" ];
    UIImage* image = [UIImage imageWithContentsOfFile:path];
    return image;
}
Poli
fuente
1

Aquí hay un código para descargar una imagen de forma asíncrona desde la url y luego guardar donde desee en el objetivo-c: ->

    + (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
        {
            NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
            [NSURLConnection sendAsynchronousRequest:request
                                               queue:[NSOperationQueue mainQueue]
                                   completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                                       if ( !error )
                                       {
                                           UIImage *image = [[UIImage alloc] initWithData:data];
                                           completionBlock(YES,image);
                                       } else{
                                           completionBlock(NO,nil);
                                       }
                                   }];
        }
Mohd. Como si
fuente
0

Si está utilizando la biblioteca AFNetworking para descargar la imagen y las imágenes se utilizan en UITableview, puede utilizar el siguiente código en cellForRowAtIndexPath

 [self setImageWithURL:user.user_ProfilePicturePath toControl:cell.imgView]; 
 
-(void)setImageWithURL:(NSURL*)url toControl:(id)ctrl
{
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
        if (image) {
            if([ctrl isKindOfClass:[UIButton class]])
            {
                UIButton btn =(UIButton)ctrl;
                [btn setBackgroundImage:image forState:UIControlStateNormal];
            }
            else
            {
                UIImageView imgView = (UIImageView)ctrl;
                imgView.image = image;
            }

} } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { NSLog(@"No Image"); }]; [operation start];}
Ashisht
fuente
0

Puede descargar la imagen sin bloquear la interfaz de usuario con NSURLSessionDataTask.

+(void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
 {
 NSURLSessionDataTask*  _sessionTask = [[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:url]
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error != nil)
        {
          if ([error code] == NSURLErrorAppTransportSecurityRequiresSecureConnection)
                {
                    completionBlock(NO,nil);
                }
         }
    else
     {
      [[NSOperationQueue mainQueue] addOperationWithBlock: ^{
                        dispatch_async(dispatch_get_main_queue(), ^{
                        UIImage *image = [[UIImage alloc] initWithData:data];
                        completionBlock(YES,image);

                        });

      }];

     }

                                            }];
    [_sessionTask resume];
}
Mohd. Como si
fuente
0

Aquí hay una solución Swift 5 para descargar y guardar una imagen o en general un archivo en el directorio de documentos usando Alamofire:

func dowloadAndSaveFile(from url: URL) {
    let destination: DownloadRequest.DownloadFileDestination = { _, _ in
        var documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        documentsURL.appendPathComponent(url.lastPathComponent)
        return (documentsURL, [.removePreviousFile])
    }
    let request = SessionManager.default.download(url, method: .get, to: destination)
    request.validate().responseData { response in
        switch response.result {
        case .success:
            if let destinationURL = response.destinationURL {
                print(destinationURL)
            }
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
}
tanaschita
fuente