tableView cell image which is asynchronous loaded flickers as you scroll fast

12,869

Solution 1

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).

Solution 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!

Solution 3

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.

Share:
12,869
user2920762
Author by

user2920762

Updated on July 03, 2022

Comments

  • user2920762
    user2920762 almost 2 years

    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;
    }
    
  • Rob
    Rob over 10 years
    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).
  • Nikos M.
    Nikos M. over 10 years
    UIImage *offersImage = [UIImage imageWithData:data]; this is executed on the main thread in his code.
  • Rob
    Rob over 10 years
    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.
  • Ajay Choudhary
    Ajay Choudhary over 10 years
    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.
  • Rob
    Rob over 10 years
    @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.
  • casillas
    casillas over 8 years
    @Rob, I would be glad if you take a look the following question stackoverflow.com/questions/33397726/…
  • Mamouneyya
    Mamouneyya about 8 years
    @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?
  • Rob
    Rob about 8 years
    @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.
  • Mamouneyya
    Mamouneyya about 8 years
    @Rob So you're telling me that simply calling af_setImageWithURL() from the cell's UIImageView in swift will handle everything for me?
  • Mamouneyya
    Mamouneyya about 8 years
    @Rob If you have free time, could you please leave an answer for my question here: stackoverflow.com/questions/34403664/… 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.
  • Elsammak
    Elsammak over 6 years
    I set imageview,image = nil only and it did the job!