Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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;
}
like image 491
user2920762 Avatar asked Oct 30 '13 18:10

user2920762


3 Answers

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!

like image 177
Marko Hlebar Avatar answered Oct 15 '22 21:10

Marko Hlebar


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

like image 42
Rob Avatar answered Oct 15 '22 20:10

Rob


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.

like image 2
danypata Avatar answered Oct 15 '22 21:10

danypata