web-dev-qa-db-fra.com

Chargement d'image asynchrone à partir de l'URL dans une cellule UITableView - l'image se change en mauvaise image lors du défilement

J'ai écrit deux méthodes pour charger de manière asynchrone des images dans ma cellule UITableView. Dans les deux cas, l'image se chargera bien, mais lorsque je ferai défiler le tableau, les images changeront plusieurs fois jusqu'à ce que le défilement se termine et que l'image revienne à la bonne image. Je n'ai aucune idée pourquoi cela se produit.

#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

- (void)viewDidLoad
{
    [super viewDidLoad];
    dispatch_async(kBgQueue, ^{
        NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:
                                                       @"http://myurl.com/getMovies.php"]];
        [self performSelectorOnMainThread:@selector(fetchedData:)
                               withObject:data waitUntilDone:YES];
    });
}

-(void)fetchedData:(NSData *)data
{
    NSError* error;
    myJson = [NSJSONSerialization
              JSONObjectWithData:data
              options:kNilOptions
              error:&error];
    [_myTableView reloadData];
}    

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    // Return the number of rows in the section.
    // Usually the number of items in your array (the one that holds your list)
    NSLog(@"myJson count: %d",[myJson count]);
    return [myJson count];
}
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

        myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        if (cell == nil) {
            cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
        }

        dispatch_async(kBgQueue, ^{
        NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];

            dispatch_async(dispatch_get_main_queue(), ^{
        cell.poster.image = [UIImage imageWithData:imgData];
            });
        });
         return cell;
}

......

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

            myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
            if (cell == nil) {
                cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
            }
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]];
    NSURLRequest* request = [NSURLRequest requestWithURL:url];


    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse * response,
                                               NSData * data,
                                               NSError * error) {
                               if (!error){
                                   cell.poster.image = [UIImage imageWithData:data];
                                   // do whatever you want with image
                               }

                           }];
     return cell;
}
146
Segev

En supposant que vous cherchiez une solution tactique rapide, assurez-vous que l'image de la cellule est initialisée et que la ligne de la cellule est toujours visible, par exemple:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];

    cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];

    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];

    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data) {
            UIImage *image = [UIImage imageWithData:data];
            if (image) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                    if (updateCell)
                        updateCell.poster.image = image;
                });
            }
        }
    }];
    [task resume];

    return cell;
}

Le code ci-dessus résout quelques problèmes liés au fait que la cellule est réutilisée:

  1. Vous n'initialisez pas l'image de la cellule avant de lancer la demande d'arrière-plan (ce qui signifie que la dernière image de la cellule retirée de la file d'attente sera toujours visible pendant le téléchargement de la nouvelle image). Assurez-vous de nil la propriété image de toute vue d'image, sinon vous verrez le scintillement des images.

  2. Un problème plus subtil est que, sur un réseau très lent, votre demande asynchrone risque de ne pas se terminer avant que la cellule ne disparaisse de l'écran. Vous pouvez utiliser la méthode UITableViewcellForRowAtIndexPath: (à ne pas confondre avec la même méthode UITableViewDataSourcetableView:cellForRowAtIndexPath:) pour voir si la cellule de cette ligne est toujours visible. Cette méthode renverra nil si la cellule n'est pas visible.

    Le problème est que la cellule a été désactivée au moment où votre méthode asynchrone est terminée et, pire, que la cellule a été réutilisée pour une autre ligne du tableau. En vérifiant si la ligne est toujours visible, vous vous assurez que vous ne mettez pas accidentellement l'image à jour avec l'image d'une ligne qui a depuis disparu de l'écran.

  3. Peu liée à la question, je me suis toujours sentie obligée de la mettre à jour pour tirer parti des conventions et des API modernes, notamment:

    • Utilisez NSURLSession plutôt que de distribuer -[NSData contentsOfURL:] dans une file d'attente en arrière-plan;

    • Utilisez dequeueReusableCellWithIdentifier:forIndexPath: plutôt que dequeueReusableCellWithIdentifier: (mais veillez à utiliser un prototype de cellule ou une classe de registre ou une NIB pour cet identifiant); et

    • J’ai utilisé un nom de classe conforme à Conventions de dénomination Cocoa (c’est-à-dire qui commence par la lettre majuscule).

Même avec ces corrections, il y a des problèmes:

  1. Le code ci-dessus ne met pas en cache les images téléchargées. Cela signifie que si vous faites défiler une image hors de l'écran et revenez à l'écran, l'application peut essayer de récupérer à nouveau l'image. Vous aurez peut-être la chance que les en-têtes de réponse de votre serveur permettent la mise en cache assez transparente offerte par NSURLSession et NSURLCache, mais sinon, vous ferez des requêtes de serveur inutiles et vous offrirez une UX beaucoup plus lente.

  2. Nous n'annulons pas les demandes de cellules qui sortent de l'écran. Ainsi, si vous passez rapidement à la 100e ligne, l’image de cette ligne risque d’être en retard sur les demandes des 99 lignes précédentes qui ne sont même plus visibles. Vous voulez toujours vous assurer de hiérarchiser les demandes de cellules visibles pour le meilleur UX.

Le correctif le plus simple permettant de résoudre ces problèmes consiste à utiliser une catégorie UIImageView, comme celle fournie avec SDWebImage ou AFNetworking . Si vous le souhaitez, vous pouvez écrire votre propre code pour traiter les problèmes ci-dessus, mais cela demande beaucoup de travail et les catégories UIImageView ci-dessus l'ont déjà fait pour vous.

215
Rob

/ * Je l'ai fait de cette façon et l'ai aussi testé * /

Étape 1 = Enregistrez une classe de cellule personnalisée (dans le cas d'une cellule prototype dans un tableau) ou un nib (dans le cas d'une nib personnalisée pour une cellule personnalisée) pour un tableau comme celui-ci dans la méthode viewDidLoad:

[self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"];

OR

[self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"];

Étape 2 = Utilisez la méthode "dequeueReusableCellWithIdentifier: forIndexPath:" de UITableView comme ceci (pour cela, vous devez enregistrer la classe ou le nib):

   - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
            CustomTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell" forIndexPath:indexPath];

            cell.imageViewCustom.image = nil; // [UIImage imageNamed:@"default.png"];
            cell.textLabelCustom.text = @"Hello";

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                // retrive image on global queue
                UIImage * img = [UIImage imageWithData:[NSData dataWithContentsOfURL:     [NSURL URLWithString:kImgLink]]];

                dispatch_async(dispatch_get_main_queue(), ^{

                    CustomTableViewCell * cell = (CustomTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
                  // assign cell image on main thread
                    cell.imageViewCustom.image = img;
                });
            });

            return cell;
        }
14
Nitesh Borad

Plusieurs cadres permettent de résoudre ce problème. Juste pour en nommer quelques-uns:

Rapide:

Objectif c:

13
kean

Swift 3

J'écris ma propre implémentation légère pour le chargeur d'images avec NSCache . Pas de scintillement d'image cellulaire!

ImageCacheLoader.Swift

typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ())

class ImageCacheLoader {

    var task: URLSessionDownloadTask!
    var session: URLSession!
    var cache: NSCache<NSString, UIImage>!

    init() {
        session = URLSession.shared
        task = URLSessionDownloadTask()
        self.cache = NSCache()
    }

    func obtainImageWithPath(imagePath: String, completionHandler: @escaping ImageCacheLoaderCompletionHandler) {
        if let image = self.cache.object(forKey: imagePath as NSString) {
            DispatchQueue.main.async {
                completionHandler(image)
            }
        } else {
            /* You need placeholder image in your assets, 
               if you want to display a placeholder to user */
            let placeholder = #imageLiteral(resourceName: "placeholder")
            DispatchQueue.main.async {
                completionHandler(placeholder)
            }
            let url: URL! = URL(string: imagePath)
            task = session.downloadTask(with: url, completionHandler: { (location, response, error) in
                if let data = try? Data(contentsOf: url) {
                    let img: UIImage! = UIImage(data: data)
                    self.cache.setObject(img, forKey: imagePath as NSString)
                    DispatchQueue.main.async {
                        completionHandler(img)
                    }
                }
            })
            task.resume()
        }
    }
}

Exemple d'utilisation

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier")

    cell.title = "Cool title"

    imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in
        // Before assigning the image, check whether the current cell is visible
        if let updateCell = tableView.cellForRow(at: indexPath) {
            updateCell.imageView.image = image
        }
    }    
    return cell
}
4

La meilleure réponse n’est pas la bonne façon de procéder :(. Vous avez en réalité lié indexPath au modèle, ce qui n’est pas toujours bon. Imaginez que certaines lignes aient été ajoutées lors du chargement de l’image. La cellule correspondant à indexPath existe à l’écran, mais l’image n'est plus correct! La situation est peu probable et difficile à reproduire, mais c'est possible.

Il est préférable d'utiliser l'approche MVVM, de lier cellule avec viewModel dans le contrôleur et de charger l'image dans viewModel (attribution du signal ReactiveCocoa avec la méthode switchToLatest), puis de souscrire ce signal et d'attribuer une image à la cellule! ;)

Vous devez vous rappeler de ne pas abuser de MVVM. Les vues doivent être simples comme bonjour! Alors que ViewModels devrait être réutilisable! C'est pourquoi il est très important de lier View (UITableViewCell) et ViewModel dans le contrôleur.

3
badeleux

Voici la version de Swift (en utilisant le code @Nitesh Borad Objective C): -

   if let img: UIImage = UIImage(data: previewImg[indexPath.row]) {
                cell.cardPreview.image = img
            } else {
                // The image isn't cached, download the img data
                // We should perform this in a background thread
                let imgURL = NSURL(string: "webLink URL")
                let request: NSURLRequest = NSURLRequest(URL: imgURL!)
                let session = NSURLSession.sharedSession()
                let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
                    let error = error
                    let data = data
                    if error == nil {
                        // Convert the downloaded data in to a UIImage object
                        let image = UIImage(data: data!)
                        // Store the image in to our cache
                        self.previewImg[indexPath.row] = data!
                        // Update the cell
                        dispatch_async(dispatch_get_main_queue(), {
                            if let cell: YourTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? YourTableViewCell {
                                cell.cardPreview.image = image
                            }
                        })
                    } else {
                        cell.cardPreview.image = UIImage(named: "defaultImage")
                    }
                })
                task.resume()
            }
3
Chathuranga Silva

Dans mon cas, cela n'était pas dû à la mise en cache des images (Used SDWebImage). C'était à cause de la discordance de balise de cellule personnalisée avec indexPath.row.

Sur cellForRowAtIndexPath: 

1) Attribuez une valeur d'index à votre cellule personnalisée. Par exemple,

cell.tag = indexPath.row

2) Sur le fil principal, avant d’attribuer l’image, vérifiez si l’image appartient à la cellule correspondante en la faisant correspondre à la balise.

dispatch_async(dispatch_get_main_queue(), ^{
   if(cell.tag == indexPath.row) {
     UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
     thumbnailImageView.image = tmpImage;
   }});
});
3
A.G
 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
        MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];

        cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];

        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];

        NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (data) {
                UIImage *image = [UIImage imageWithData:data];
                if (image) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                        if (updateCell)
                            updateCell.poster.image = image;
                    });
                }
            }
        }];
        [task resume];

        return cell;
    }
2
Dharmraj Vora

Merci "Rob" .... J'ai eu le même problème avec UICollectionView et ta réponse m'aide à résoudre mon problème .

 if ([Dict valueForKey:@"ImageURL"] != [NSNull null])
    {
        cell.coverImageView.image = nil;
        cell.coverImageView.imageURL=nil;

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            if ([Dict valueForKey:@"ImageURL"] != [NSNull null] )
            {
                dispatch_async(dispatch_get_main_queue(), ^{

                    myCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];

                    if (updateCell)
                    {
                        cell.coverImageView.image = nil;
                        cell.coverImageView.imageURL=nil;

                        cell.coverImageView.imageURL=[NSURL URLWithString:[Dict valueForKey:@"ImageURL"]];

                    }
                    else
                    {
                        cell.coverImageView.image = nil;
                        cell.coverImageView.imageURL=nil;
                    }


                });
            }
        });

    }
    else
    {
        cell.coverImageView.image=[UIImage imageNamed:@"default_cover.png"];
    }
1
sneha

Il suffit de changer,

dispatch_async(kBgQueue, ^{
     NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:   [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
     dispatch_async(dispatch_get_main_queue(), ^{
        cell.poster.image = [UIImage imageWithData:imgData];
     });
 });

Dans

    dispatch_async(kBgQueue, ^{
         NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:   [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
         cell.poster.image = [UIImage imageWithData:imgData];
         dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
         });
     });
0

Je pense que vous souhaitez accélérer le chargement de votre cellule au moment du chargement de l'image pour la cellule en arrière-plan. Pour cela nous avons effectué les étapes suivantes:

  1. La vérification du fichier existe ou non dans le répertoire du document.

  2. Sinon, chargez l'image pour la première fois et enregistrez-la dans le répertoire de documents de votre téléphone Si vous ne souhaitez pas enregistrer l'image dans le téléphone, vous pouvez charger des images de cellules directement en arrière-plan.

  3. Maintenant, le processus de chargement:

Incluez simplement: #import "ManabImageOperations.h"

Le code est comme ci-dessous pour une cellule:

NSString *imagestr=[NSString stringWithFormat:@"http://www.yourlink.com/%@",[dictn objectForKey:@"member_image"]];

        NSString *docDir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
        NSLog(@"Doc Dir: %@",docDir);

        NSString  *pngFilePath = [NSString stringWithFormat:@"%@/%@",docDir,[dictn objectForKey:@"member_image"]];

        BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath];
        if (fileExists)
        {
            [cell1.memberimage setImage:[UIImage imageWithContentsOfFile:pngFilePath] forState:UIControlStateNormal];
        }
        else
        {
            [ManabImageOperations processImageDataWithURLString:imagestr andBlock:^(NSData *imageData)
             {
                 [cell1.memberimage setImage:[[UIImage alloc]initWithData: imageData] forState:UIControlStateNormal];
                [imageData writeToFile:pngFilePath atomically:YES];
             }];
}

ManabImageOperations.h:

#import <Foundation/Foundation.h>

    @interface ManabImageOperations : NSObject
    {
    }
    + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
    @end

ManabImageOperations.m:

#import "ManabImageOperations.h"
#import <QuartzCore/QuartzCore.h>
@implementation ManabImageOperations

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

    dispatch_queue_t callerQueue = dispatch_get_main_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);
        });
    });
  //  downloadQueue=nil;
    dispatch_release(downloadQueue);

}
@end

Veuillez vérifier la réponse et commenter si un problème survient ....

0
Manab Kumar Mal

Vous pouvez simplement passer votre URL,

NSURL *url = [NSURL URLWithString:@"http://www.myurl.com/1.png"];
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data,    NSURLResponse * _Nullable response, NSError * _Nullable error) {
    if (data) {
        UIImage *image = [UIImage imageWithData:data];
        if (image) {
            dispatch_async(dispatch_get_main_queue(), ^{
                    yourimageview.image = image;
            });
        }
    }
}];
[task resume];
0
User558