web-dev-qa-db-fra.com

Téléchargement asynchrone d'images pour UITableView avec GCD

Je télécharge des images pour ma uitableview de manière asynchrone à l'aide de GCD, mais il existe un problème - lors du défilement des images, le scintillement et la modification en permanence. J'ai essayé de régler l'image sur nil avec chaque cellule, mais cela n'aide pas beaucoup . Lors du défilement rapide, toutes les images sont fausses. Que puis-je faire à ce sujet? Voici ma méthode pour les cellules:

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {

                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];

                UIImage* image = [[UIImage alloc] initWithData:imageData];

                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = image;
                    [cell setNeedsLayout];
                     });
            });

    cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }
    return cell;
}
34
Dvole

Le problème ici est que vos blocs de récupération d'images contiennent des références aux cellules de la table. Une fois le téléchargement terminé, il définit la propriété imageView.image, même si vous avez recyclé la cellule pour afficher une ligne différente.

Vous aurez besoin de votre bloc de fin de téléchargement pour vérifier si l'image est toujours pertinente pour la cellule avant de définir l'image.

Il est également intéressant de noter que vous ne stockez pas les images ailleurs que dans la cellule. Vous devrez donc les télécharger à nouveau chaque fois que vous faites défiler une ligne à l'écran. Vous souhaiterez probablement les mettre en cache quelque part et rechercher des images mises en cache localement avant de commencer un téléchargement.

Edit: voici un moyen simple de tester, en utilisant la propriété tag de la cellule:

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

    cell.tag = indexPath.row;
    NSDictionary *parsedData = self.loader.parsedData[indexPath.row];
    if (parsedData)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(queue, ^(void) {

            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:parsedData[@"imageLR"]];

            UIImage* image = [[UIImage alloc] initWithData:imageData];
            if (image) {
                 dispatch_async(dispatch_get_main_queue(), ^{
                     if (cell.tag == indexPath.row) {
                         cell.imageView.image = image;
                         [cell setNeedsLayout];
                     }
                 });
             }
        });

        cell.textLabel.text = parsedData[@"id"];
    }
    return cell;
}
95
Seamus Campbell

Le fait est que vous n'avez pas bien compris le concept de réutilisation de cellules. Cela ne va pas très bien avec les téléchargements asynchrones. 

Le bloc 

    ^{
    cell.imageView.image = image;
    [cell setNeedsLayout];
}

est exécuté lorsque la demande est terminée et que toutes les données ont été chargées. Mais la cellule obtient sa valeur lorsque le bloc est créé.

Au moment de l'exécution du bloc, la cellule pointe toujours sur l'une des cellules existantes. Mais il est fort probable que l'utilisateur a poursuivi le défilement. L'objet cellule a été réutilisé entre temps et l'image est associée à une cellule ' old ' qui est réutilisée, affectée et affichée. Peu de temps après, l'image correcte est chargée, attribuée et affichée, à moins que l'utilisateur n'ait davantage fait défiler l'écran. Ainsi de suite. 

Vous devriez rechercher un moyen plus intelligent de le faire. Il y a beaucoup de monuments. Google pour le chargement d'images paresseux. 

7
Hermann Klecker

Utilisez le chemin d'index pour obtenir la cellule. Si ce n'est pas visible, la cellule sera nil et vous n'aurez pas de problème. Bien sûr, vous souhaiterez probablement mettre en cache les données une fois téléchargées afin de définir l'image de la cellule immédiatement lorsque vous avez déjà l'image.

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {
                //  You may want to cache this explicitly instead of reloading every time.
                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];
                UIImage* image = [[UIImage alloc] initWithData:imageData];
                dispatch_async(dispatch_get_main_queue(), ^{
                    // Capture the indexPath variable, not the cell variable, and use that
                    UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
                    blockCell.imageView.image = image;
                    [blockCell setNeedsLayout];
                });
            });
        cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }

    return cell;
}
6
Carl Veazey

J'ai étudié ce problème et j'ai trouvé une excellente approche en personnalisant un UITableViewCell.

#import <UIKit/UIKit.h>

@interface MyCustomCell : UITableViewCell

@property (nonatomic, strong) NSURLSessionDataTask *imageDownloadTask;
@property (nonatomic, weak) IBOutlet UIImageView *myImageView;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView *activityIndicator;

@end

Maintenant, dans votre TableViewController, déclarez deux propriétés pour NSURLSessionConfiguration et NSURLSession et initialisez-les dans ViewDidLoad:

@interface MyTableViewController ()

@property (nonatomic, strong) NSURLSessionConfiguration *sessionConfig;
@property (nonatomic, strong) NSURLSession *session;
.
.
.
@end

@implementation TimesVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    _session = [NSURLSession sessionWithConfiguration:_sessionConfig];
}

.
.
.

Supposons que votre source de données est un tableau NSMutableDictionary (ou NSManagedObjectContext). Vous pouvez facilement télécharger une image pour chaque cellule, avec la mise en cache, comme ceci:

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

    if (!cell)
    {
        cell = [[MyCustomCell alloc] initWithStyle:UITableViewCellStyleDefault
                                reuseIdentifier:@"cell"];
    }

    NSMutableDictionary *myDictionary = [_myArrayDataSource objectAtIndex:indexPath.row];    

    if (cell.imageDownloadTask)
    {
        [cell.imageDownloadTask cancel];
    }

    [cell.activityIndicator startAnimating];
    cell.myImageView.image = nil;

    if (![myDictionary valueForKey:@"image"])
    {
        NSString *urlString = [myDictionary valueForKey:@"imageURL"];
        NSURL *imageURL = [NSURL URLWithString:urlString];
        if (imageURL)
        {
            cell.imageDownloadTask = [_session dataTaskWithURL:imageURL
                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
            {
                if (error)
                {
                    NSLog(@"ERROR: %@", error);
                }
                else
                {
                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

                    if (httpResponse.statusCode == 200)
                    {
                        UIImage *image = [UIImage imageWithData:data];

                        dispatch_async(dispatch_get_main_queue(), ^{
                            [myDictionary setValue:data forKey:@"image"];
                            [cell.myImageView setImage:image];
                            [cell.activityIndicator stopAnimating];
                        });
                    }
                    else
                    {
                        NSLog(@"Couldn't load image at URL: %@", imageURL);
                        NSLog(@"HTTP %d", httpResponse.statusCode);
                    }
                }
            }];

            [cell.imageDownloadTask resume];
        }
    }
    else
    {
        [cell.myImageView setImage:[UIImage imageWithData:[myDictionary valueForKey:@"image"]]];
        [cell.activityIndicator stopAnimating];
    }

    return cell;
}

J'espère que cela aide certains développeurs!.

CRÉDITS: Table View Images in iOS 7

3
Tom Calmon

Ne réinventez pas la roue, utilisez simplement ces quelques gousses impressionnantes: 

https://github.com/rs/SDWebImage

https://github.com/JJSaccolo/UIActivityIndicator-for-SDWebImage

Aussi simple que: 

    [self.eventImage setImageWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", [SchemeConfiguration APIEndpoint] , _event.imageUrl]]
                          placeholderImage:nil
                                 completed:^(UIImage *image,
                                             NSError *error,
                                             SDImageCacheType cacheType,
                                             NSURL *imageURL)
     {
         event.image = UIImagePNGRepresentation(image);
     } usingActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
1
halbano

Avez-vous pensé à utiliser SDWebImage? Il télécharge également l'image de l'URL donnée de manière asynchrone. Je n'ai pas utilisé la totalité de la bibliothèque (importé uniquement UIImageView + WebCache.h). Une fois importé, il suffit d’appeler la méthode en tant que telle:

[UIImageXYZ sd_setImageWithURL:["url you're retrieving from"] placeholderImage:[UIImage imageNamed:@"defaultProfile.jpeg"]];

Il se peut que vous utilisiez trop AFNetworking 2.0, mais cela a fonctionné pour moi.

Voici le lien vers le github si vous voulez l'essayer

1
RockyEEKlvn

Outre la réponse acceptée de Seamus Campbell vous devriez également savoir que cela ne fonctionne pas toujours. Dans ce cas, nous devrions recharger la cellule spécifique. Alors

if (image) {
     dispatch_async(dispatch_get_main_queue(), ^{
          if (cell.tag == indexPath.row) {
               cell.imageView.image = image;
               [cell setNeedsLayout];
          }
      });
 }

devrait être changé en,

    if (image) {
         dispatch_async(dispatch_get_main_queue(), ^{
              if (cell.tag == indexPath.row) {
                   cell.imageView.image = image;
                   self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
              }
          });
     }
1

Pour ceux qui s'inquiètent de la modification des chemins d'index et du manque de cohérence, une solution potentielle consiste à sous-classer UITableViewCell ou UICollectionViewCell et à ajouter une variable d'instance appelée quelque chose comme stringTag. Ensuite, placez l'URL de la photo que vous téléchargez dans le stringTag. Lorsque vous définissez l'image, vérifiez si le stringTag est toujours l'URL correcte.

Voir cette réponse pour plus de détails: Extraction asynchrone terminée: la cellule est-elle toujours affichée? .

Voici mes cours: 

import Foundation
import UIKit

class ImageAsyncCollectionViewCell : UICollectionViewCell {
  var stringTag: String?
}

et ensuite lors de l'utilisation de la cellule:

    cell.stringTag = photoKey
    cell.imageView.image = self.blankImage
    if ImageCache.default.imageCachedType(forKey: photoKey).cached {
      ImageCache.default.retrieveImage(forKey: photoKey, options: nil) {
        image, cacheType in
        if let image = image {
          DispatchQueue.main.async {
            if cell.stringTag == photoKey {
              cell.imageView.image = image
            }
          }
        } else {
          print("Doesn't exist in cache, but should")
          self.setCellWithPhoto(photoKey: photoKey, cell: cell)
        }
      }
    } else {
      self.setCellWithPhoto(photoKey: photoKey, cell: cell)
    }
0
Ronak Vora