Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView reloads wrong images on scroll

In the code below, I download image URLs from a text file, store it in a NSMutableArray, then download the images from from those URLS in CellforItemAtIndexPath. However, when I scroll up and down, for a brief moment, the wrong image is in the wrong place. It corrects a second later but I would like to get rid of that scrolling problem. Here's the code:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    static NSString *identifier = @"Cell2";

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
    AsyncImageView *recipeImageView = (AsyncImageView *)[cell viewWithTag:100];
    UILabel *titleView = (UILabel *)[cell viewWithTag:101];
    titleView.numberOfLines = 3;
    UIImageView *overlay = (UIImageView *)[cell viewWithTag:111];
    FXBlurView *blurView = (FXBlurView *)[cell viewWithTag:110];
    blurView.tintColor = [UIColor colorWithRed:51.0 green:51.0 blue:51.0 alpha:1];


    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        hi = [self videoTitle];
        images = [self firstPhoto];
        // switch to a background thread and perform your expensive operation


        // switch back to the main thread to update your UI


        dispatch_async(dispatch_get_main_queue(), ^{
            NSString *imgSrc;

            NSString *url = images[indexPath.row];
            imgSrc = url;
            if (imgSrc != nil && [imgSrc length] != 0 ) {
                [recipeImageView setImageWithURL:[NSURL URLWithString:images[indexPath.row]] placeholderImage:[UIImage imageNamed:@"red.png"]];

            }
            else {
                NSLog(@"noimage");
                recipeImageView.image = [UIImage imageNamed:@"red.png"];
            }
            titleView.text = hi[indexPath.row];


        });
    });

    return cell;


    // recipeImageView.image = [UIImage imageNamed:[videoList objectAtIndex:indexPath.row]];

}

Any ideas? Thank you.

like image 608
user1941966 Avatar asked Feb 19 '14 15:02

user1941966


2 Answers

I noticed a bunch of downvotes without comments and after reading my answer again I guess the original accepted answer (below the line) could have been better.

There are a few things going on with these kinds of problems. The UITableView only creates just enough UITableViewCells to display every cell in view plus what's going to scroll into view. The other cells are going to get re-used after they scroll out of view.

This means when you're scrolling fast the cell gets set up multiple times to fetch an image async and then display it because it's pretty slow fetching the image. After the image loads it will show itself on the cell that originally called to fetch it, but that cell might be re-used already.

Nowadays I have a bufferedImage: UIImage? property on whatever data class I use as data for the UITableViewCells. This will initially always be nil so I fetch the image async and when the dispatch_async returns it will store it there so the next time I need to show this cell it's ready, and if it's still the same cell I will also display it in the cell.

So the right way to do it is:

  • Start configuring the cell with your data object
  • Store the data object in your cell so it can be referred later
  • Check if the data object already has an image set in bufferedImage and if so display it and that's it
  • If there's no image yet, reset the current image to nothing or a placeholder and start downloading the image in the background
  • When the async call returns store the image in the bufferedImage property
  • Check if the object that is stored in the current cell is still the same object you fetched the image for (this is very important), if not don't do anything
  • If you're still in the same cell display the image

So there's one object you pass along in your async call so you can store the fetched image. There's another object that's in the current cell that you can compare to. Often the cell already got reused before you got the image, so those objects are different by the time your image is downloaded.


Scrolling and rotating CollectionViews always has been a bit problematic. I always have the same problem, images that appear in the wrong cells. There is some kind of optimization going on for the collectionview that affects images but not labels. To me it's a bug but it still isn't solved after three iOS releases.

You need to implement the following:

https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewLayout_class/Reference/Reference.html#//apple_ref/occ/instm/UICollectionViewLayout/shouldInvalidateLayoutForBoundsChange:

This should always return YES basically to always refresh it. If this solves your problem then you could look into not executing the whole redraw every time as it's bad for performance. I personally spent a lot of time in counting if a screen had passed or line had passed and so on but it became stuttery instead so I ended up just returning YES. YMMV.

like image 41
Lucas van Dongen Avatar answered Nov 22 '22 06:11

Lucas van Dongen


I'm not sure what AsyncImageView is, so first let me answer your question as if it were a UIImageView:


In your callback block (when you go back to the main queue), you are referring to recipeImageView. However, if the user has scrolled during download, the cell object has been reused.

Once you're in the callback, you have to assume that all of your references to views (cell, recipeImageView, blurView, etc. are pointing to the wrong thing. You need to find the correct views using your data model.

One common approach is using the NSIndexPath to find the cell:

UICollectionViewCell *correctCell = [collectionView cellForItemAtIndexPath:indexPath]:
UIImageView *correctRecipeImageView = (UIImageView *)[correctCell viewWithTag:100];
/* Update your image now… */

Note that this will only work if one indexPath is always guaranteed to point at the same content - if the user can insert/delete rows, then you'll need to figure out the correct indexPath too, since it may have changed as well:

NSIndexPath *correctIndexPath  = /* Use your data model to find the correct index path */
UICollectionViewCell *correctCell = [collectionView cellForItemAtIndexPath:correctIndexPath]:
UIImageView *correctRecipeImageView = (UIImageView *)[correctCell viewWithTag:100];
/* Update your image now… */

That said, if AsyncImageView is some other class that's supposed to handle all of this for you, then you can just call setImageWithURL: placeholderImage: on the main thread, and it should handle all the asynchronous loading on your behalf. If it doesn't, I'd like to recommend the SDWebImage library, which does: https://github.com/rs/SDWebImage

like image 145
Aaron Brager Avatar answered Nov 22 '22 06:11

Aaron Brager