0

I have a table view with custom cells. Every cell has an image, title, and a description. When I first load the table, it loads fine. if I slowly scroll trough the images, also seems to work fine. As soon as I scroll down fast (assuming the number of cells is large enough to not fit in without scrolling up and down) the images start to changes cells in a random order. Some cells have the same image twice.

Any clue why this is happening?

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

    NewsArticle *currArt = [self.lN_Dept objectAtIndex:indexPath.row];

    if(currArt.artImage == Nil)
    {
        if([currArt.mainImage_URL rangeOfString:@"<img src="].location != NSNotFound)
        {
            NSRange range = [currArt.mainImage_URL rangeOfString:@"<img src=\"/CONC/"];
            NSString *substring = [[currArt.mainImage_URL substringFromIndex:NSMaxRange(range)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

            NSRange range2 = [substring rangeOfString:@"\""];
            NSString *substring2 = [[substring substringToIndex:NSMaxRange(range2)-1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

            NSString *imageURL = [PUB_URL stringByAppendingString:substring2];

            [self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
                if (succeeded) {
                    currArt.artImage = image;
                    cell.ArtDisplayImage.image = currArt.artImage;
                }
            }];
        }
    }
    else
    {
        cell.ArtDisplayImage.image = currArt.artImage;
    }
    [cell configureCellForEntry:currArticle];
    return cell;
}


- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request
                                   queue:[NSOperationQueue mainQueue]
                       completionHandler:^(NSURLResponse *response, NSData *Data, NSError *error) {
                           if ( !error )
                           {
                               UIImage *image = [[UIImage alloc] initWithData:Data];
                               completionBlock(YES,image);
                           } else{
                               completionBlock(NO,nil);
                           }
                       }];
}
dan
  • 23
  • 7
  • Have a look at my answer here: http://stackoverflow.com/questions/7852033/gcd-uitableview-asynchronous-load-images-wrong-cells-are-loaded-until-new-image/7852138#7852138 – hypercrypt Mar 18 '15 at 14:16
  • @hypercrypt Could you provide an example how would you do it with the code above? – dan Mar 18 '15 at 15:13

3 Answers3

2

It happens because you misunderstand how UITableView works. Let's imagine your table view has 100 cells and it can display 10 cells simultaneously. When table view loads, it creates 10 instances of your cells. When you start scrolling down through the table view it actually doesn't creates new instances of your cells – it reuses cells that have disappeared from the screen (because you are calling [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];). That means that the cell at index 10 will have the same reference that the cell at index 0.
Back to your question you are loading images with

[self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
            if (succeeded) {
                currArt.artImage = image;
                cell.ArtDisplayImage.image = currArt.artImage;
            }
}];

In case the image was downloaded faster then the cell was reused (which happens when your are scrolling slowly) everything will be alright. But in case cell was reused before image was downloaded then there are two blocks in memory that are downloading images for that cell. To solve this issue I would recommend you to use SDWebImage which handles this situations automatically or cancel downloading of images for cells which are disappeared from the screen. Hope this will help.

Nikita Arkhipov
  • 1,208
  • 4
  • 11
  • 28
  • How would you suggest I cancel downloading an image for cells which are disappeared from the screen? – dan Mar 18 '15 at 15:10
1

There's probably a race condition here. A download is queued up for a given cell and then that cell is reused and another image is set or download is queued but then the first download completes and sets the image even though the cell represents a different element now. If you want to see if this is indeed the issue, disable cell reuse (instantiate a new cell for every row). If the problem goes away, then that was it.

One thing you can do is cancel an existing download that was started by a given cell if that cell is reused. Check out the open source library SDWebImage which handles this issue this way.

Another thing you can do is have the cell store the URL it is currently trying to load and compare the URL of the resulting download against the cell's stored URL to see if they are still the same (and only set the image if they are still the same).

Something like this should do it:

if(currArt.artImage == Nil)
{
    if([currArt.mainImage_URL rangeOfString:@"<img src="].location != NSNotFound)
    {
        NSRange range = [currArt.mainImage_URL rangeOfString:@"<img src=\"/CONC/"];
        NSString *substring = [[currArt.mainImage_URL substringFromIndex:NSMaxRange(range)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

        NSRange range2 = [substring rangeOfString:@"\""];
        NSString *substring2 = [[substring substringToIndex:NSMaxRange(range2)-1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

        NSString *imageURL = [PUB_URL stringByAppendingString:substring2];
        cell.imageURL = imageURL; // you need to add an imageURL string property to your cells
        // you need to return the URL string in your block to compare it
        [self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image, NSString *url) 
        {
            if (succeeded) 
            {
                currArt.artImage = image;
                if(cell.imageURL isEqualToString:url])
                {
                    cell.ArtDisplayImage.image = currArt.artImage;
                }
            }
        }];
    }
}
else
{
    cell.ArtDisplayImage.image = currArt.artImage;
    cell.imageURL = nil; // make sure it is not overwritten
}
Dima
  • 23,484
  • 6
  • 56
  • 83
  • I added another function to my post. could you please tell me how to modify that so it does what you suggested. to compare the url. thanks – dan Mar 18 '15 at 14:03
1

So something like this:

[self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
            if (succeeded) {

                YourCellType *cellToUpdate = [tableView cellForIndexPath:indexPath];
                if (cellToUpdate)
                {
                    currArt.artImage = image;
                    cellToUpdate.ArtDisplayImage.image = currArt.artImage;
                }
            }
        }];
hypercrypt
  • 15,389
  • 6
  • 48
  • 59