Search code examples
iosswiftuicollectionview

How to access off-screen but existing cells in UICollectionView in Swift?


The title might be a little hard to understand, but this situation might help you with it.

I'm writing a multiple image picker. Say the limit is 3 pictures, after the user has selected 3, all other images will have alpha = 0.3 to indicate that this image is not selectable. (Scroll all the way down to see a demo)

First of all, this is the code I have:

PickerPhotoCell (a custom collection view cell):

class PickerPhotoCell: UICollectionViewCell {
    @IBOutlet weak var imageView: UIImageView!

    var selectable: Bool {
        didSet {
            self.alpha = selectable ? 1 : 0.3
        }
    }
}

PhotoPickerViewController:

class PhotoPickerViewController: UICollectionViewController {

    ...
    var photos: [PHAsset]()    // Holds all photo assets
    var selected: [PHAsset]()    // Holds all selected photos
    var limit: Int = 3

    override func viewDidLoad() {
        super.viewDidLoad()

        // Suppose I have a func that grabs all photos from photo library
        photos = grabAllPhotos()
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell ...
        let asset = photos[indexPath.row]

        ...

        // An image is selectable if:
        // 1. It's already selected, then user can deselect it, or
        // 2. Number of selected images are < limit
        cell.selectable = cell.isSelected || selected.count < limit

        return cell
    }

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let cell = collectionView.cellForItem(at: indexPath) as! PickerPhotoCell

        if cell.isSelected {
            // Remove the corresponding PHAsset in 'selected' array
        } else {
            // Append the corresponding PhAsset to 'selected' array
        }

        // Since an image is selected/deselected, I need to update
        // which images are selectable/unselectable now
        for visibleCell in collectionView.visibleCells {
            let visiblePhoto = visibleCell as! PickerPhotoCell
            visiblePhoto.selectable = visiblePhoto.isSelected || selected.count < limit
        }
    }
}

This works almost perfectly, except for one thing, look at the GIF:

enter image description here

The problem is

After I've selected 3 photos, all other visible photos have alpha = 0.3, but when I scroll down a little more, there are some photos that still have alpha = 1. I know why this is happening - Because they were off-screen, calling collectionView.visibleCells wouldn't affect them & unlike other non-existing cells, they did exist even though they were off-screen. So I wonder how I could access them and therefore make them unselectable?


Solution

  • The problem is that you are trying to store your state in the cell itself, by doing this: if cell.isSelected.... There are no off screen cells in the collection view, it reuses cells all the time, and you should actually reset cell's state in prepareForReuse method. Which means you need to store your data outside of the UICollectionViewCell. What you can do is store selected IndexPath in your view controller's property, and use that data to mark your cells selected or not.

    pseudocode:

    class MyViewController {
    
        var selectedIndexes = [IndexPath]()
    
        func cellForItem(indexPath) {
           cell.isSelected =  selectedIndexes.contains(indexPath)
        }
    
        func didSelectCell(indexPath) {
            if selectedIndexes.contains(indexPath) {
                selectedIndexes.remove(indexPath)
            } else if selectedIndexes.count < limiit {
                selectedIndexes.append(indexPath)
            }
        }
    }