Fast Scrolling UICollectionView displays invalid images in a cell when new items are added asynchronously

I have a UICollectionView that displays images in a grid, but when scrolling quickly, it displays the wrong image in the cell instantly until the image is loaded from the S3 repository and then the correct image is displayed.

I saw questions and answers related to this issue on SO, but none of the solutions work for me. The let items = [[String: Any]]() dictionary is populated after an API call. I need a cell to remove an image from a recycled cell. Now there is an unpleasant effect of the "dance" of the image.

Here is my code:

 var searchResults = [[String: Any]]() let cellId = "cellId" override func viewDidLoad() { super.viewDidLoad() collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout) collectionView.delegate = self collectionView.dataSource = self collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId) func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell let item = searchResults[indexPath.item] cell.backgroundColor = .white cell.itemLabel.text = item["title"] as? String let imageUrl = item["img_url"] as! String let url = URL(string: imageUrl) let request = Request(url: url!) cell.itemImageView.image = nil Nuke.loadImage(with: request, into: cell.itemImageView) return cell } class MyCollectionViewCell: UICollectionViewCell { var itemImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.layer.cornerRadius = 0 imageView.clipsToBounds = true imageView.translatesAutoresizingMaskIntoConstraints = false imageView.backgroundColor = .white return imageView }() override init(frame: CGRect) { super.init(frame: frame) --------- } override func prepareForReuse() { super.prepareForReuse() itemImageView.image = nil } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } 

I changed the image loading per cell with the following code:

 cell.itemImageView.image = nil APIManager.sharedInstance.myImageQuery(url: imageUrl) { (image) in guard let cell = collectionView.cellForItem(at: indexPath) as? MyCollectionViewCell else { return } cell.itemImageView.image = image cell.activityIndicator.stopAnimating() } 

Here is my API manager.

 struct APIManager { static let sharedInstance = APIManager() func myImageQuery(url: String, completionHandler: @escaping (UIImage?) -> ()) { if let url = URL(string: url) { Manager.shared.loadImage(with: url, token: nil) { // internal to Nuke guard let image = $0.value as UIImage? else { return completionHandler(nil) } completionHandler(image) } } } 

If the user scrolls past the content limit, more items will be loaded in my collection view. This seems to be the root of the problem when cell reuse is reused. Other fields in the cell, such as the name of the item, are also replaced when new items are loaded.

 func scrollViewDidScroll(_ scrollView: UIScrollView) { if (scrollView.contentOffset.y + view.frame.size.height) > (scrollView.contentSize.height * 0.8) { loadItems() } } 

Here is my data upload function

 func loadItems() { if loadingMoreItems { return } if noMoreData { return } if(!Utility.isConnectedToNetwork()){ return } loadingMoreItems = true currentPage = currentPage + 1 LoadingOverlay.shared.showOverlay(view: self.view) APIManager.sharedInstance.getItems(itemURL, page: currentPage) { (result, error) -> () in if error != nil { } else { self.parseData(jsonData: result!) } self.loadingMoreItems = false LoadingOverlay.shared.hideOverlayView() } } func parseData(jsonData: [String: Any]) { guard let items = jsonData["items"] as? [[String: Any]] else { return } if items.count == 0 { noMoreData = true return } for item in items { searchResults.append(item) } for index in 0..<self.searchResults.count { let url = URL(string: self.searchResults[index]["img_url"] as! String) let request = Request(url: url!) self.preHeatedImages.append(request) } self.preheater.startPreheating(with: self.preHeatedImages) DispatchQueue.main.async { // appendCollectionView(numberOfItems: items.count) self.collectionView.reloadData() self.collectionView.collectionViewLayout.invalidateLayout() } } 
+7
ios swift3 uicollectionview uicollectionviewcell
source share
3 answers

When the collection view scrolls the cell outside the bounds, viewing the collection can reuse the cell to display another element. That's why you get cells from a method called dequeue Reusable Cell(withReuseIdentifier:for:) .

You need to make sure that when the image is ready, the image will still be displayed on that image.

I recommend that you change your Nuke.loadImage(with:into:) method to make a closure instead of presenting the image:

 struct Nuke { static func loadImage(with request: URLRequest, body: @escaping (UIImage) -> ()) { // ... } } 

Thus, in collectionView(_:cellForItemAt:) you can load the image as follows:

 Nuke.loadImage(with: request) { [weak collectionView] (image) in guard let cell = collectionView?.cellForItem(at: indexPath) as? MyCollectionViewCell else { return } cell.itemImageView.image = image } 

If the collection view no longer displays the item in any cell, the image will be discarded. If the collection view displays an item in any cell (even in another cell), you will save the image in the correct image format.

+6
source share

Try the following:

Whenever a cell is reused, the prepareForReuse method is prepareForReuse . You can reset your cell here. In your case, you can set the default image in the image view here before loading the original image.

  override func prepareForReuse() { super.prepareForReuse() self.imageView.image = UIImage(named: "DefaultImage" } 
+3
source share

On the Nuke project page:

Nuke.loadImage (using the: in :) method cancels the previous issued request associated with the object. Nuke has a weak reference to the target, when the target is released, the related request is automatically canceled.

And as far as I can tell, you are using your sample code correctly:

 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { ... cell.imageView.image = nil Nuke.loadImage(with: url, into: cell.imageView) ... } 

You explicitly set the UIImageViews image to zero, so there really shouldn't be a way to display the image until Nuke loads the new image into the target.

Nuke will also cancel the previous request. I use Nuke myself and do not have these problems. Are you sure your “modified code” is not malfunctioning here? Everything seems right to me, and I see no problem why this should not work.

One possible problem might be reloading the entire collection view. We should always try to provide a better user interface, so if you change the data source in the collection view, try using performBatchUpdates to animate insertions / updates / moves / deletes.

A good library that can take care of this automatically, like Dwifft.

0
source share

All Articles