10

Im using a asynchronous block (Grand central dispatch) to load my cell images. However if you scroll fast they still appear but very fast until it has loaded the correct one. Im sure this is a common problem but I can not seem to find a away around it.

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


// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];

    dispatch_async(dispatch_get_main_queue(), ^{

        UIImage *offersImage = [UIImage imageWithData:data];

        cell.imageView.image = offersImage;
    });
});

cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];

return cell;
}
user2920762
  • 223
  • 1
  • 8
  • 17
  • See http://stackoverflow.com/questions/19326284/what-is-the-proper-way-to-use-nscache-with-dispatch-async-in-a-reusable-table-ce/19327472#19327472 or http://stackoverflow.com/questions/16663618/async-image-loading-from-url-inside-a-uitableview-cell-image-changes-to-wrong/16663759#16663759 – Rob Oct 30 '13 at 18:34
  • Please, consider my post http://stackoverflow.com/a/26147814/3052059. Thank you! – Thomás Pereira Oct 03 '14 at 13:18
  • I would recommend reading this blog post http://www.natashatherobot.com/how-to-download-images-asynchronously-and-make-your-uitableview-scroll-fast-in-ios/ – pprochazka72 Jan 06 '16 at 16:28

4 Answers4

18

At the very least, you probably want to remove the image from the cell (in case it is a re-used cell) before your dispatch_async:

cell.imageView.image = [UIImage imageNamed:@"placeholder.png"];

Or

cell.imageView.image = nil;

You also want to make sure that the cell in question is still on screen before updating (by using the UITableView method, cellForRowAtIndexPath: which returns nil if the cell for that row is no longer visible, not to be confused with the UITableViewDataDelegate method tableView:cellForRowAtIndexPath:), e.g.:

static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

cell.imageView.image = [UIImage imageNamed:@"placeholder.png"];

// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];

    if (data) {
        UIImage *offersImage = [UIImage imageWithData:data];
        if (offersImage) {
            dispatch_async(dispatch_get_main_queue(), ^{
                UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];

                if (updateCell) {
                    updateCell.imageView.image = offersImage;
                }
            });
        }
    }
});

cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];

return cell;

Frankly, you should also be using a cache to avoid re-retrieving images unnecessarily (e.g. you scroll down a bit and scroll back up, you don't want to issue network requests for those prior cells' images). Even better, you should use one of the UIImageView categories out there (such as the one included in SDWebImage or AFNetworking). That achieves the asynchronous image loading, but also gracefully handles cacheing (don't reretrieve an image you just retrieved a few seconds ago), cancelation of images that haven't happened yet (e.g. if user quickly scrolls to row 100, you probably don't want to wait for the first 99 to retrieve before showing the user the image for the 100th row).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • There is also a potential issue where a cell could be reused before the asynchronous call completes, resulting in the correct image being replaced with an image from a previous call. Asynchronously loading images for tableview cells isn't trivial. – kubi Oct 30 '13 at 18:51
  • @kubi Agreed, but my code sample addressed that issue by checking `updateCell`, to make sure the cell wasn't reused. But you're right that it's non-trivial and my tactical fix to the immediate issue doesn't address a variety of other issues (cache, backlogged requests, etc.), hence my suggestion to use one of those `UIImageView` categories, which makes it _much_ easier. – Rob Oct 30 '13 at 18:55
  • @Rob, I would be glad if you take a look the following question http://stackoverflow.com/questions/33397726/download-images-from-a-server-to-display-on-the-collectionview – casillas Oct 28 '15 at 18:41
  • @Rob I'm really curious why would using one of the UIImageView categories solves the mentioned issues. As I understand, all what theses libraries do is making the requesting / caching things easier. However, they won't solve issues like cell reusing, changing indexPaths (e.g. by dynamically adding/removing cells while the table loaded), backlogged requests, etc. Do they? – Mamouneyya Feb 13 '16 at 12:35
  • @Mamouneyya - They actually do handle all of those issues. They handle changing index paths because the requested image is associated with the actual image view, not any index path, so it's impervious to inserted/deleted rows above it. They handle reuse, because when the cell is reused and you request an image for the new row, it cancels the previous request for that reused image view before starting the new one. They handled backlog of requests for the same reason, namely requests for reused cells (i.e. all those other cells that have scrolled off screen) are cancelled. – Rob Feb 13 '16 at 16:09
  • @Rob So you're telling me that simply calling af_setImageWithURL() from the cell's UIImageView in swift will handle everything for me? – Mamouneyya Feb 14 '16 at 12:25
  • @Rob If you have free time, could you please leave an answer for my question here: http://stackoverflow.com/questions/34403664/using-alamofireimage-inside-uitableviewcell cause the first comment there states different facts. I would really appreciate if you clear things out there, as that would help anyone from the community. – Mamouneyya Feb 14 '16 at 12:31
2

This is a problem with async image loading... Let's say you have 5 visible rows at any given time. If you are scrolling fast, and you scroll down for instance 10 rows, the tableView:cellForRowAtIndexPath will be called 10 times. The thing is that these calls are faster than the images are returned, and you have the 10 pending images from different URL-s. When the images finally come back from the server, they will be returned on those cells that you put in the async loader. Since you are reusing only 5 cells, some of these images will be displayed twice on each cell, as they are downloaded from the server, and that is why you see flickering. Also, remember to call

cell.imageView.image = nil

before calling the async downloading method, as the previous image from the reused cell will remain and also cause a flickering effect when the new image is assigned.

The way around this is to store the latest URL of the image you have to display on each cell, and then when the image comes back from the server check that URL with the one you have in your request. If it is not the same, cache that image for later. For caching requests, check out NSURLRequest and NSURLConnection classes.

I strongly suggest that you use AFNetworking for any server communication though.

Good luck!

Marko Hlebar
  • 1,973
  • 17
  • 18
2

The reason of your flicker is that your start the download for several images during the scrolling, every time you a cell is displayed on screen a new request is performed and it's possible that the old requests are not completed, every tile a request completes the image is set on the cell, so it's if you scroll fast you use let's say a cell 3 times = 3 requests that will be fired = 3 images will be set on that cell = flicker.

I had the same issue and here is my approach: Create a custom cell with all the required views. Each cells has it's own download operation. In the cell's -prepareForReuse method. I would make the image nil and cancel the request.

In this way for each cell I have only one request operation = one image = no flicker.

Even using AFNetworking you can have the same issue if you won't cancel the image download.

danypata
  • 9,895
  • 1
  • 31
  • 44
0

The issue is that you download the image on the main thread. Dispatch a background queue for that:

dispatch_queue_t myQueue = dispatch_queue_create("myQueue", NULL);

    // execute a task on that queue asynchronously
    dispatch_async(myQueue, ^{
      NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
//UI updates should remain on the main thread
UIImage *offersImage = [UIImage imageWithData:data];
    dispatch_async(dispatch_get_main_queue(), ^{

        UIImage *offersImage = [UIImage imageWithData:data];

        cell.imageView.image = offersImage;
    });
    });
Nikos M.
  • 13,685
  • 4
  • 47
  • 61
  • user2920762 is not downloading the image on the main thread. He's using a global (i.e. background) queue for that (which is not great, but better than the main queue). He's simply not handling the reuse of a cell correctly (nor properly dealing with the notion that the cell might have scrolled off screen by the time the asynchronous retrieval is done). – Rob Oct 30 '13 at 18:39
  • `UIImage *offersImage = [UIImage imageWithData:data];` this is executed on the main thread in his code. – Nikos M. Oct 30 '13 at 18:40
  • Agreed. (As an aside you're actually doing `imageWithData` twice, once in background queue and again on main queue, which I'm sure you didn't intend.) The main problem, though, is the retrieval on a background queue takes so long that he's seeing the old image while the new one downloads (and not handling the fact that the cell could have be reused for another row while the image was being downloaded). So, you're right, that you should do `imageWithData` in the background queue, but that won't solve the problem. The problem is a failure to initialize `cell.imageView.image` before the dispatch. – Rob Oct 30 '13 at 18:45