Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView images mix up on scrolling

Tags:

ios

swift

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
}
like image 422
Mitul Jindal Avatar asked Dec 05 '25 14:12

Mitul Jindal


2 Answers

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.

like image 181
Gary Makin Avatar answered Dec 07 '25 04:12

Gary Makin


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.

like image 33
shim Avatar answered Dec 07 '25 05:12

shim



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!