I am making an application where a single cell of the Collection takes up the entire screen. Each cell contains an image. These images are downloaded from the server and stored in a custom class (Card) as UIImage. When I am displaying the images from an array of that class objects.
When I scroll, the images sometime flash at wrong cells. How do I correct it?
CollectionViewController.swift
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ListCell", for: indexPath) as! ListViewCell
cell.configure(card: ClientSingleton.cards[indexPath.row])
cell.index = indexPath.row
return cell
}
ListViewCell.swift
override func prepareForReuse() {
super.prepareForReuse()
imageView?.image = UIImage()
}
func configure(card: Card) {
imageView?.image = UIImage()
if let image = card.image {
self.image = image
self.setupImageView()
self.setupGyroBar()
self.setupGyro()
} else {
DispatchQueue.global(qos: .userInitiated).async {
card.loadImage() { image in
DispatchQueue.main.async {
self.image = image
self.setupImageView()
self.setupGyroBar()
self.setupGyro()
}
}
}
}
self.edgeColor = card.edgeColor
self.inverseEdgeColor = card.inverseEdgeColor
self.backgroundColor = self.edgeColor
}
Cells get reused as they scroll out of view. During a configure call, while an image is being loaded, the cell may be scrolled out of view and be reused.
The easiest solution is to have some identifier in the cell that you check when the image loading has completed. You need to check that the identifier is still what you expect it to be.
CollectionViewController.swift
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ListCell", for: indexPath) as! ListViewCell
// Pass the row to the configure call so it can be used as an identifier
cell.configure(card: ClientSingleton.cards[indexPath.row], forIndex:indexPath.row)
return cell
}
ListViewCell.swift
override func prepareForReuse() {
super.prepareForReuse()
imageView?.image = UIImage()
}
func configure(card: Card, forIndex index: Int) {
// Save the index as the identifier as the first thing you do,
// then check it in the async call.
self.index = index
imageView?.image = UIImage()
if let image = card.image {
self.image = image
self.setupImageView()
self.setupGyroBar()
self.setupGyro()
} else {
DispatchQueue.global(qos: .userInitiated).async {
card.loadImage() { image in
DispatchQueue.main.async {
if self.index == index {
// The cell is still being used for this index
self.image = image
self.setupImageView()
self.setupGyroBar()
self.setupGyro()
}
// else it is being used for another, so do not set the image
}
}
}
}
self.edgeColor = card.edgeColor
self.inverseEdgeColor = card.inverseEdgeColor
self.backgroundColor = self.edgeColor
}
Note that this code will only work if there is a correspondence between the row and the image to be shown. If this is not the case, you will need to use a better identifier for checking that the cell is still the correct one.
Presumably since you're asynchronously loading images, the cells may still be in the middle of a request when they get reused. You're setting the image to an empty UIImage when they're preparing for reuse, but the request may finish after that and then soon before or after start another loading request.
Probably the easiest fix is if you can cancel the request (or ignore its result) when the cells are getting reused. One potential solution is OperationQueue, which you could use to queue and cancel the operation as needed.
Or if you're using some networking library it may provide some useful methods for cancelling a request that is in progress.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With