UICollectionView reloads wrong images on scroll
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.
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