web-dev-qa-db-fra.com

Chargement d'une image dans UIImage de manière asynchrone

Je développe une application iOS 4 avec le SDK iOS 5.0 et XCode 4.2.

Je dois montrer quelques articles de blog dans un UITableView. Lorsque j'ai récupéré toutes les données du service Web, j'utilise cette méthode pour créer un UITableViewCell:

- (BlogTableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString* cellIdentifier = @"BlogCell";

    BlogTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil)
    {
        NSArray* topLevelObjects =  [[NSBundle mainBundle] loadNibNamed:@"BlogTableViewCell" owner:nil options:nil];

        for(id currentObject in topLevelObjects)
        {
            if ([currentObject isKindOfClass:[BlogTableViewCell class]])
            {
                cell = (BlogTableViewCell *)currentObject;
                break;
            }
        }
    }

    BlogEntry* entry = [blogEntries objectAtIndex:indexPath.row];

    cell.title.text = entry.title;
    cell.text.text = entry.text;
    cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];

    return cell;
}

Mais cette ligne:

cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];

c'est tellement lent (entry.photo a une URL http).

Existe-t-il un moyen de charger cette image de manière asynchrone? Je pense que c'est difficile car tableView: cellForRowAtIndexPath est appelé très souvent.

36
VansFannel

Jetez un œil à SDWebImage:

https://github.com/rs/SDWebImage

C'est un ensemble fantastique de classes qui s'occupent de tout pour vous.

Tim

28
tarmes

J'ai écrit une classe personnalisée pour ce faire, en utilisant des blocs et GCD:

WebImageOperations.h

#import <Foundation/Foundation.h>

@interface WebImageOperations : NSObject {
}

// This takes in a string and imagedata object and returns imagedata processed on a background thread
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
@end

WebImageOperations.m

#import "WebImageOperations.h"
#import <QuartzCore/QuartzCore.h>

@implementation WebImageOperations


+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
{
    NSURL *url = [NSURL URLWithString:urlString];

    dispatch_queue_t callerQueue = dispatch_get_current_queue();
    dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
    dispatch_async(downloadQueue, ^{
        NSData * imageData = [NSData dataWithContentsOfURL:url];

        dispatch_async(callerQueue, ^{
            processImage(imageData);
        });
    });
    dispatch_release(downloadQueue);
}

@end

Et dans votre ViewController

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    // Pass along the URL to the image (or change it if you are loading there locally)
    [WebImageOperations processImageDataWithURLString:entry.photo andBlock:^(NSData *imageData) {
    if (self.view.window) {
        UIImage *image = [UIImage imageWithData:imageData];

        cell.photo.image = image;
    }

    }];
}

Il est très rapide et chargera les images sans affecter l'interface utilisateur ou la vitesse de défilement de TableView.

*** Remarque - Cet exemple suppose que l'ARC est utilisé. Sinon, vous devrez gérer vos propres versions sur les objets)

81
LJ Wilson

Dans iOS 6 et versions ultérieures, dispatch_get_current_queue donne des avertissements de dépréciation.

Voici une alternative qui est une synthèse de la réponse @ElJay ci-dessus et de l'article de @khanlou ici .

Créez une catégorie sur UIImage:

UIImage + Helpers.h

@interface UIImage (Helpers)

+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback;

@end

UIImage + Helpers.m

#import "UIImage+Helpers.h"

@implementation UIImage (Helpers)

+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        NSData * imageData = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            UIImage *image = [UIImage imageWithData:imageData];
            callback(image);
        });
    });
}

@end
52
bbrame

Rapide:

extension UIImage {

    // Loads image asynchronously
    class func loadFromURL(url: NSURL, callback: (UIImage)->()) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {

            let imageData = NSData(contentsOfURL: url)
            if let data = imageData {
                dispatch_async(dispatch_get_main_queue(), {
                    if let image = UIImage(data: data) {
                        callback(image)
                    }
                })
            }
        })
    }
}

Usage:

// First of all remove the old image (required for images in cells)
imageView.image = nil 

// Load image and apply to the view
UIImage.loadFromURL("http://...", callback: { (image: UIImage) -> () in
     self.imageView.image = image
})
6
Alexander Volkov

Considérant le cas d'échec.

- (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        NSError * error = nil;
        NSData * imageData = [NSData dataWithContentsOfURL:url options:0 error:&error];
        if (error)
            callback(nil);

        dispatch_async(dispatch_get_main_queue(), ^{
            UIImage *image = [UIImage imageWithData:imageData];
            callback(image);
        });
    });
}
3
jose920405

Swift 4 | Chargement asynchrone de l'image

Créez une nouvelle classe nommée ImageLoader.Swift

import UIKit

class ImageLoader {

var cache = NSCache<AnyObject, AnyObject>()

class var sharedInstance : ImageLoader {
    struct Static {
        static let instance : ImageLoader = ImageLoader()
    }
    return Static.instance
}

func imageForUrl(urlString: String, completionHandler:@escaping (_ image: UIImage?, _ url: String) -> ()) {
        let data: NSData? = self.cache.object(forKey: urlString as AnyObject) as? NSData

        if let imageData = data {
            let image = UIImage(data: imageData as Data)
            DispatchQueue.main.async {
                completionHandler(image, urlString)
            }
            return
        }

    let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL.init(string: urlString)!) { (data, response, error) in
        if error == nil {
            if data != nil {
                let image = UIImage.init(data: data!)
                self.cache.setObject(data! as AnyObject, forKey: urlString as AnyObject)
                DispatchQueue.main.async {
                    completionHandler(image, urlString)
                }
            }
        } else {
            completionHandler(nil, urlString)
        }
    }
    downloadTask.resume()
    }
}

Pour utiliser dans votre classe ViewController:

ImageLoader.sharedInstance.imageForUrl(urlString: "https://www.logodesignlove.com/images/classic/Apple-logo-rob-janoff-01.jpg", completionHandler: { (image, url) in
                if image != nil {
                    self.imageView.image = image
                }
            })
3
Akshay Sarda

Oui, c'est relativement facile. L'idée est quelque chose comme:

  1. Créez un tableau mutable pour contenir vos images
  2. Remplissez le tableau avec des espaces réservés si vous le souhaitez (ou des objets NSNull si vous ne le souhaitez pas)
  3. Créez une méthode pour récupérer vos images de manière asynchrone en arrière-plan
  4. Lorsqu'une image est arrivée, échangez votre espace réservé avec l'image réelle et effectuez une [self.tableView reloadData]

J'ai testé cette approche à plusieurs reprises et donne d'excellents résultats. Si vous avez besoin d'aide/d'exemples avec la partie asynchrone (je recommande d'utiliser gcd pour cela) faites le moi savoir.

2
Alladinian

vous pouvez facilement le faire parfaitement si vous utilisez l'exemple de code fourni par Apple à cet effet: l'exemple de code: Lazy Image

Regardez simplement le délégué rowforcell et ajoutez des fichiers icondownloader à votre projet.

Le seul changement que vous devez faire est de changer un objet d'enregistrement avec votre objet.

1
Firas KADHUM

Bien que SDWebImage et d'autres solutions tierces soient peut-être d'excellentes solutions, si vous ne souhaitez pas utiliser des API tierces, vous pouvez également développer votre propre solution.

Reportez-vous à ce tutoriel sur le chargement paresseux qui explique également comment modéliser vos données dans la vue tableau.

1
Nirav Bhatt

Vous devrez probablement sous-classer votre UIImageView. J'ai récemment fait un projet simple pour expliquer cette tâche particulière - chargement d'image asynchrone en arrière-plan - jetez un œil à mon projet sur GitHub. Plus précisément, regardez la classe KDImageView .

0
Kyr Dunenkoff