it-swarm.com.de

Asynchrones Herunterladen von Bildern für UITableView mit GCD

Ich lade asynchron Bilder für meine Nutzbarkeitsansicht mithilfe von GCD herunter, aber es gibt ein Problem - beim Scrollen von Bildern flimmern und ändern sie sich ständig. Ich habe versucht, das Bild mit jeder Zelle auf Null zu setzen, aber es hilft nicht viel .. Wenn Sie schnell zurückblättern, sind alle Bilder falsch. Was kann ich dagegen tun? Hier ist meine Methode für Zellen:

- (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

Das Problem hierbei ist, dass Ihre Bildabrufblöcke Verweise auf die Tableview-Zellen enthalten. Wenn der Download abgeschlossen ist, wird die imageView.image -Eigenschaft festgelegt, auch wenn Sie die Zelle wiederverwendet haben, um eine andere Zeile anzuzeigen.

Sie benötigen Ihren Download-Abschlussblock, um zu testen, ob das Bild für die Zelle noch relevant ist, bevor Sie das Bild einstellen.

Es ist auch erwähnenswert, dass Sie die Bilder nicht an einem anderen Ort als in der Zelle speichern. Sie werden also jedes Mal erneut heruntergeladen, wenn Sie eine Zeile auf dem Bildschirm scrollen. Sie möchten sie wahrscheinlich irgendwo im Cache speichern und nach lokal zwischengespeicherten Images suchen, bevor Sie einen Download starten.

Bearbeiten: Hier ist eine einfache Methode zum Testen mithilfe der tag-Eigenschaft der Zelle:

- (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

Der Punkt ist, dass Sie das Konzept der Wiederverwendung von Zellen nicht vollständig verstanden haben. Das stimmt nicht gut mit asynchronen Downloads überein. 

Der Block 

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

wird ausgeführt, wenn die Anforderung abgeschlossen ist und alle Daten geladen wurden. Die Zelle erhält jedoch ihren Wert, wenn der Block erstellt wird.

Zu dem Zeitpunkt, zu dem der Block ausgeführt wird, zeigt die Zelle immer noch auf eine der vorhandenen Zellen. Es ist jedoch sehr wahrscheinlich, dass der Benutzer weiter scrollte. Das Zellobjekt wurde zwischenzeitlich wiederverwendet und das Bild ist einer ' old ' - Zelle zugeordnet, die erneut verwendet und zugewiesen und angezeigt wird. Kurz danach wird das richtige Bild geladen und zugewiesen und angezeigt, sofern der Benutzer nicht weiter gescrollt hat. und so weiter und so weiter. 

Sie sollten nach einer intelligenteren Methode suchen. Es gibt viele Turorials. Google zum faulen Laden von Bildern. 

7
Hermann Klecker

Verwenden Sie den Indexpfad, um die Zelle abzurufen. Wenn es nicht sichtbar ist, ist die Zelle nil und Sie haben kein Problem. Natürlich sollten Sie die Daten beim Herunterladen zwischenspeichern, um das Bild der Zelle sofort einzustellen, wenn Sie das Bild bereits haben.

- (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

Ich habe mich mit diesem Problem beschäftigt und habe durch Anpassen einer UITableViewCell einen hervorragenden Ansatz gefunden.

#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

Deklarieren Sie nun in Ihrem TableViewController zwei Eigenschaften für NSURLSessionConfiguration und NSURLSession und initialisieren Sie diese in 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];
}

.
.
.

Nehmen wir an, Ihre Datenquelle ist ein Array von NSMutableDictionary (oder NSManagedObjectContext). Sie können das Bild für jede Zelle einfach herunterladen, mit Zwischenspeicherung wie folgt:

.
.
.
- (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;
}

Ich hoffe, es hilft einigen Entwicklern!.

CREDITS: Table View-Bilder in iOS 7

3
Tom Calmon

Das Rad nicht neu erfinden, sondern nur ein paar tolle Hülsen verwenden: 

https://github.com/rs/SDWebImage

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

So einfach wie: 

    [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

Haben Sie daran gedacht, SDWebImage zu verwenden? Es lädt das Bild auch asynchron von der angegebenen URL herunter. Ich habe nicht die gesamte Bibliothek verwendet (nur UIImageView + WebCache.h importiert). Nach dem Import müssen Sie die Methode nur als solche aufrufen:

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

Es könnte übertrieben sein, wenn Sie AFNetworking 2.0 verwenden, aber es hat für mich geklappt.

Hier ist der Link zum Github, wenn du es versuchen willst

1
RockyEEKlvn

Neben akzeptierter Antwort von Seamus Campbell sollten Sie auch wissen, dass dies manchmal nicht funktioniert. In diesem Fall sollten wir die spezifische Zelle neu laden. So

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

sollte geändert werden in

    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

Für diejenigen, die sich Sorgen machen, dass sich Indexpfade ändern und nicht konsistent sind, ist eine mögliche Lösung die Unterklasse UITableViewCell oder UICollectionViewCell und das Hinzufügen einer Instanzvariablen mit dem Namen stringTag. Geben Sie dann die URL des Fotos, das Sie herunterladen, in das Zeichenfolge-Tag ein. Prüfen Sie beim Festlegen des Bildes, ob das stringTag noch die richtige URL ist.

Weitere Informationen finden Sie in dieser Antwort: Asynchroner Abruf abgeschlossen: Wird die Zelle noch angezeigt? .

Hier sind meine Klassen: 

import Foundation
import UIKit

class ImageAsyncCollectionViewCell : UICollectionViewCell {
  var stringTag: String?
}

und dann bei Verwendung der Zelle:

    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