164 votes

Chargement asynchrone d'une image à partir d'une URL dans une cellule d'un UITableView - l'image devient erronée lors du défilement.

J'ai écrit deux méthodes pour charger de manière asynchrone des images dans la cellule de mon UITableView. Dans les deux cas, l'image se charge correctement mais lorsque je fais défiler le tableau, les images changent plusieurs fois jusqu'à ce que le défilement se termine et que l'image revienne à la bonne image. Je n'ai aucune idée de la raison pour laquelle 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;
}

6 votes

Vous essayez de stocker des informations dans les cellules actuelles. C'est mauvais, très mauvais. Vous devez stocker les informations dans un tableau (ou quelque chose de similaire) et les afficher ensuite dans les cellules. Dans ce cas, l'information est l'UIImage réelle. Oui, chargez-la de manière asynchrone, mais chargez-la dans un tableau.

1 votes

@Fogmeister Faites-vous référence à poster ? Il s'agit vraisemblablement d'une image dans sa cellule personnalisée, donc ce que fait EXEC_BAD_ACCESS est parfaitement correct. Vous avez raison de dire que vous ne devez pas utiliser la cellule comme référentiel pour les données du modèle, mais je ne pense pas que ce soit ce qu'il fait. Il donne simplement à la cellule personnalisée ce dont elle a besoin pour se présenter. En outre, et c'est une question plus subtile, je serais prudent quant au stockage d'une image, elle-même, dans votre tableau de modèle soutenant votre tableview. Il est préférable d'utiliser un mécanisme de mise en cache de l'image et votre objet modèle doit récupérer à partir de ce cache.

1 votes

Oui, c'est exactement ce que je veux dire. En regardant la requête (qui est affichée en entier), il télécharge l'image de manière asynchrone et la place directement dans l'imageView de la cellule. (Il utilise donc la cellule pour stocker les données, c'est-à-dire l'image). Ce qu'il devrait faire, c'est référencer un objet et demander l'image à cet objet (contenu dans un tableau ou autre). Si l'objet n'a pas encore l'image, il devrait renvoyer un espace réservé et télécharger l'image. Ensuite, lorsque l'image est téléchargée et prête à être affichée, il faut le faire savoir au tableau pour qu'il puisse mettre à jour la cellule (si elle est visible).

241voto

Rob Points 70987

En supposant que vous cherchiez une solution tactique rapide, vous devez vous assurer 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 découlant du fait que la cellule est réutilisée :

  1. Vous n'initialisez pas l'image de la cellule avant de lancer la requête en arrière-plan (ce qui signifie que la dernière image de la cellule mise en file d'attente sera toujours visible pendant le téléchargement de la nouvelle image). Veillez à nil le site image de toutes les vues d'images, sinon vous verrez le scintillement des images.

  2. Un problème plus subtil est que sur un réseau très lent, votre requête asynchrone peut ne pas se terminer avant que la cellule ne défile à l'écran. Vous pouvez utiliser la fonction UITableView méthode cellForRowAtIndexPath: (à ne pas confondre avec le programme similaire intitulé UITableViewDataSource méthode tableView:cellForRowAtIndexPath: ) pour voir si la cellule de cette ligne est toujours visible. Cette méthode renvoie nil si la cellule n'est pas visible.

    Le problème est que la cellule a défilé avant que votre méthode asynchrone ne soit terminée et, pire encore, 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 à jour l'image avec l'image d'une ligne qui a depuis défilé hors de l'écran.

  3. Sans rapport avec la question posée, je me suis senti obligé de mettre à jour ce document pour tirer parti des conventions et API modernes, notamment :

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

    • Utilice dequeueReusableCellWithIdentifier:forIndexPath: plutôt que dequeueReusableCellWithIdentifier: (mais veillez à utiliser le prototype de cellule ou la classe de registre ou la NIB pour cet identifiant)

    • J'ai utilisé un nom de classe conforme à Conventions de dénomination Cocoa (c'est-à-dire commencer 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 que vous revenez à l'écran, l'application peut essayer de récupérer l'image à nouveau. Peut-être aurez-vous la chance que les en-têtes de réponse de votre serveur permettent la mise en mémoire cache assez transparente offerte par la technologie NSURLSession y NSURLCache Mais si ce n'est pas le cas, vous ferez des demandes inutiles au serveur et offrirez une interface utilisateur beaucoup plus lente.

  2. Nous n'annulons pas les demandes de cellules qui défilent hors de l'écran. Ainsi, si vous faites rapidement défiler l'écran jusqu'à la 100e ligne, l'image de cette ligne pourrait être bloquée derrière les demandes des 99 lignes précédentes qui ne sont même plus visibles. Il faut toujours s'assurer de donner la priorité aux demandes concernant les cellules visibles pour obtenir la meilleure interface utilisateur possible.

La solution la plus simple pour régler ces problèmes est d'utiliser une UIImageView comme c'est le cas avec SDWebImage ou AFNetworking . Si vous le souhaitez, vous pouvez écrire votre propre code pour résoudre les problèmes ci-dessus, mais c'est beaucoup de travail, et l'article ci-dessus ne s'applique pas. UIImageView Les catégories ont déjà fait cela pour vous.

1 votes

Merci. Je crois que vous devez modifier votre réponse. updateCell.poster.image = nil à cell.poster.image = nil; updateCell est appelé avant qu'il ne soit déclaré.

0 votes

@EXEC_BAD_ACCESS AFNetworking est une classe générale de mise en réseau qui élimine un grand nombre de vilaines NSURLConnection la programmation. SDWebImage est un framework plus petit, qui se concentre principalement sur les images récupérées sur le web. Les deux font un assez bon travail sur leur UIImageView catégories.

1 votes

Mon application utilise beaucoup de json donc AFNetworking est définitivement la voie à suivre. Je le savais mais j'étais trop paresseux pour l'utiliser. Je suis juste admiratif de la façon dont la mise en cache fonctionne avec leur simple ligne de code. [imageView setImageWithURL:<#(NSURL *)#> placeholderImage:<#(UIImage *)#>];

15voto

Nitesh Borad Points 242

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

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

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

OU

[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 la 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;
        }

1 votes

Le fait d'appeler cellForRowAtIndexPath dans le bloc final ne provoque-t-il pas une seconde exécution de l'ensemble du processus ?

0 votes

Non. En fait, j'appelle ici la méthode cellForRowAtIndexPath de tableView. Ne confondez pas avec la méthode datasource de tableView qui porte le même nom. Si nécessaire, elle peut être appelée comme [self tableView : tableView cellForRowAtIndexPath : indexPath] ; J'espère que cela dissipera votre confusion.

2voto

sneha Points 19

Merci "Rob"....J'ai eu le même problème avec UICollectionView et votre réponse m'a aidé à résoudre mon problème. Voici mon code :

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

0 votes

Pour moi, mycell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath]; n'est jamais nulle, donc cela n'a aucun effet.

1 votes

Vous pouvez vérifier que votre cellule est visible ou non en : for(mycell *updateCell in collectionView.visibleCells) { cellVisible=YES ; } if (cellVisible) { cell.coverImageView.imageURL=[NSURL URLWithString :[Dict valueForKey:@"ImageURL"]] ; } Cela fonctionne également pour moi

0 votes

@sneha Oui, vous pouvez vérifier s'il est visible en itérant dans le fichier visibleCells comme ça, mais je soupçonne qu'utiliser [collectionView cellForItemAtIndexPath:indexPath] est plus efficace (et c'est la raison pour laquelle vous faites cet appel en premier lieu).

0voto

Manab Kumar Mal Points 93

Je pense que vous voulez 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 fait les étapes suivantes :

  1. Vérifier si le fichier existe ou non dans le répertoire du document.

  2. Si ce n'est pas le cas, il faut charger l'image pour la première fois et la sauvegarder dans le fichier le répertoire de documents du téléphone. Si vous ne voulez pas sauvegarder l'image dans le téléphone, vous pouvez charger les images des cellules directement dans l'arrière-plan.

  3. Maintenant, le processus de chargement :

Il suffit d'inclure : #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.....

-1voto

Ravindra Kishan Points 11
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    Static NSString *CellIdentifier = @"Cell";
    QTStaffViewCell *cell = (QTStaffViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    If (cell == nil)
    {

        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QTStaffViewCell" owner:self options:nil];
        cell = [nib objectAtIndex: 0];

    }

    StaffData = [self.staffArray objectAtIndex:indexPath.row];
    NSString *title = StaffData.title;
    NSString *fName = StaffData.firstname;
    NSString *lName = StaffData.lastname;

    UIFont *FedSanDemi = [UIFont fontWithName:@"Aller" size:18];
    cell.drName.text = [NSString stringWithFormat:@"%@ %@ %@", title,fName,lName];
    [cell.drName setFont:FedSanDemi];

    UIFont *aller = [UIFont fontWithName:@"Aller" size:14];
    cell.drJob.text = StaffData.job;
    [cell.drJob setFont:aller];

    if ([StaffData.title isEqualToString:@"Dr"])
    {
        cell.drJob.frame = CGRectMake(83, 26, 227, 40);
    }
    else
    {
        cell.drJob.frame = CGRectMake(90, 26, 227, 40);

    }

    if ([StaffData.staffPhoto isKindOfClass:[NSString class]])
    {
        NSURL *url = [NSURL URLWithString:StaffData.staffPhoto];
        NSURLSession *session = [NSURLSession sharedSession];
        NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url
                completionHandler:^(NSURL *location,NSURLResponse *response, NSError *error) {

      NSData *imageData = [NSData dataWithContentsOfURL:location];
      UIImage *image = [UIImage imageWithData:imageData];

      dispatch_sync(dispatch_get_main_queue(),
             ^{
                    cell.imageView.image = image;
              });
    }];
        [task resume];
    }
       return cell;}

3 votes

Les vidages de code sans aucune explication sont rarement utiles. Veuillez envisager de modifier cette réponse afin de fournir un contexte.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X