web-dev-qa-db-de.com

Async-Bild wird von einer UITableView-Zelle aus einer URL geladen - das Bild ändert sich beim Scrollen in ein falsches Bild

Ich habe zwei Möglichkeiten geschrieben, Bilder in meiner UITableView-Zelle asynchron zu laden. In beiden Fällen wird das Bild gut geladen, aber wenn ich die Tabelle scrolle, werden die Bilder einige Male geändert, bis der Bildlauf endet und das Bild zum rechten Bild zurückkehrt. Ich habe keine Ahnung, warum das so ist.

#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

Wenn Sie nach einer schnellen taktischen Lösung suchen, müssen Sie sicherstellen, dass das Zellenbild initialisiert wird und die Zeile der Zelle noch sichtbar ist, z.

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

Der obige Code behebt einige Probleme, die sich aus der Wiederverwendung der Zelle ergeben:

  1. Sie initialisieren das Zellenbild nicht, bevor Sie die Hintergrundanforderung initiieren (dh das letzte Bild der aus der Warteschlange befindlichen Zelle ist noch sichtbar, während das neue Bild heruntergeladen wird). Stellen Sie sicher, dass nil die image-Eigenschaft einer beliebigen Bildansicht ist, andernfalls werden Bilder flackern.

  2. Ein subtileres Problem ist, dass in einem wirklich langsamen Netzwerk Ihre asynchrone Anforderung möglicherweise nicht beendet wird, bevor die Zelle vom Bildschirm verschoben wird. Sie können die UITableView-Methode cellForRowAtIndexPath: verwenden (nicht zu verwechseln mit der gleichnamigen UITableViewDataSource-Methode tableView:cellForRowAtIndexPath:), um festzustellen, ob die Zelle für diese Zeile noch sichtbar ist. Diese Methode gibt nil zurück, wenn die Zelle nicht sichtbar ist.

    Das Problem ist, dass die Zelle bis zu dem Zeitpunkt, zu dem Ihre async-Methode abgeschlossen ist, gescrollt wurde und, schlimmer noch, die Zelle für eine andere Zeile der Tabelle wiederverwendet wurde. Durch Überprüfen, ob die Zeile noch sichtbar ist, stellen Sie sicher, dass Sie das Bild nicht versehentlich mit dem Bild für eine Zeile aktualisieren, die seitdem vom Bildschirm gescrollt wurde.

  3. Etwas unabhängig von der vorliegenden Frage fühlte ich mich immer noch gezwungen, diese zu aktualisieren, um moderne Konventionen und API zu nutzen, insbesondere:

    • Verwenden Sie NSURLSession, anstatt -[NSData contentsOfURL:] an eine Hintergrundwarteschlange zu senden.

    • Verwenden Sie dequeueReusableCellWithIdentifier:forIndexPath: anstelle von dequeueReusableCellWithIdentifier: (stellen Sie jedoch sicher, dass Sie für diesen Bezeichner Zellprototyp oder Registerklasse oder NIB verwenden). und

    • Ich habe einen Klassennamen verwendet, der Cocoa Namenskonventionen entspricht (d. H. Mit dem Großbuchstaben beginnen).

Auch bei diesen Korrekturen gibt es Probleme:

  1. Der obige Code speichert die heruntergeladenen Bilder nicht im Cache. Das bedeutet, dass die App möglicherweise versucht, das Bild erneut abzurufen, wenn Sie ein Bild vom Bildschirm und zurück auf dem Bildschirm scrollen. Vielleicht haben Sie das Glück, dass Ihre Server-Antwortheader die recht transparente Zwischenspeicherung von NSURLSession und NSURLCache zulassen. Andernfalls werden Sie unnötige Serveranfragen stellen und eine wesentlich langsamere UX anbieten.

  2. Wir stornieren keine Anforderungen für Zellen, die vom Bildschirm verschoben werden. Wenn Sie also schnell zur 100. Zeile blättern, kann das Bild für diese Zeile hinter den Anforderungen für die vorherigen 99 Zeilen stehen, die nicht mehr sichtbar sind. Sie möchten immer sicherstellen, dass Sie Anfragen nach sichtbaren Zellen für die beste UX priorisieren.

Die einfachste Lösung zur Behebung dieser Probleme besteht in der Verwendung einer UIImageView-Kategorie, z. B. mit SDWebImage oder AFNetworking . Wenn Sie möchten, können Sie Ihren eigenen Code schreiben, um sich mit den oben genannten Problemen zu befassen, aber es ist viel Arbeit und die oben genannten UIImageView-Kategorien haben dies bereits für Sie erledigt.

215
Rob

/ * Ich habe es so gemacht und auch getestet * /

Schritt 1 = Benutzerdefinierte Zellklasse (im Falle einer Prototypzelle in der Tabelle) oder Spitze (im Falle einer benutzerdefinierten Spitze für eine benutzerdefinierte Zelle) für eine Tabelle wie folgt in der viewDidLoad-Methode registrieren:

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

OR

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

Schritt 2 = Verwenden Sie die Methode "dequeueReusableCellWithIdentifier: forIndexPath:" von UITableView. (Dazu müssen Sie die Klasse oder die Feder registrieren):

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

Es gibt mehrere Frameworks, die dieses Problem lösen. Nur um ein paar zu nennen:

Schnell:

Ziel c:

13
kean

Swift 3

Ich schreibe meine eigene Light-Implementierung für Image Loader mit NSCache . Kein Bildflimmern der Zellen!

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

Verwendungsbeispiel

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

Die beste Antwort ist nicht der richtige Weg, dies zu tun :(. Sie haben indexPath tatsächlich mit dem Modell verbunden, was nicht immer gut ist. Stellen Sie sich vor, dass beim Laden eines Bildes einige Zeilen hinzugefügt wurden ist nicht mehr richtig! Die Situation ist unwahrscheinlich und schwer zu replizieren, aber es ist möglich.

Es ist besser, den MVVM-Ansatz zu verwenden, die Zelle mit viewModel im Controller zu binden und das Bild in viewModel zu laden (das ReactiveCocoa-Signal mit der switchToLatest-Methode zuzuweisen), dieses Signal dann zu abonnieren und das Bild einer Zelle zuzuordnen! ;)

Sie müssen daran denken, MVVM nicht zu missbrauchen. Ansichten müssen einfach sein! Während ViewModels wiederverwendbar sein sollten! Deshalb ist es sehr wichtig, View (UITableViewCell) und ViewModel im Controller zu binden.

3
badeleux

Hier ist die Swift-Version (unter Verwendung von @Nitesh Borad Objective C-Code): -

   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

In meinem Fall lag es nicht an der Zwischenspeicherung von Bildern (Used SDWebImage). Dies lag daran, dass das Tag der benutzerdefinierten Zelle nicht mit indexPath.row übereinstimmt.

Auf cellForRowAtIndexPath: 

1) Weisen Sie Ihrer benutzerdefinierten Zelle einen Indexwert zu. Zum Beispiel,

cell.tag = indexPath.row

2) Prüfen Sie vor dem Zuweisen des Bildes im Haupt-Thread, ob das Bild zu der entsprechenden Zelle gehört, indem Sie es mit dem Tag abgleichen.

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

Vielen Dank "Rob" .... Ich hatte das gleiche Problem mit UICollectionView und Ihre Antwort hilft mir bei der Lösung meines Problems ..__ Hier ist mein 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"];
    }
1
sneha

Einfach ändern,

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

in

    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

Ich denke, Sie möchten das Laden der Zelle zum Zeitpunkt des Bildladens für eine Zelle im Hintergrund beschleunigen. Dafür haben wir folgende Schritte durchgeführt:

  1. Überprüfen, ob die Datei im Dokumentverzeichnis vorhanden ist oder nicht.

  2. Wenn nicht, laden Sie das Bild zum ersten Mal und speichern es in unserem Telefondokumentverzeichnis. Wenn Sie das Bild nicht im Telefon speichern möchten, können Sie Zellbilder direkt im Hintergrund laden.

  3. Nun der Ladevorgang:

Fügen Sie einfach Folgendes hinzu: #import "ManabImageOperations.h"

Der Code ist wie folgt für eine Zelle:

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

Bitte überprüfen Sie die Antwort und kommentieren Sie, wenn ein Problem auftritt ....

0
Manab Kumar Mal

Sie können einfach Ihre URL übergeben,

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