Search code examples
iosswiftuicollectionviewuibuttonreloaddata

Button flickering when reloading data in CollectionVIew


I am updating the category image by button tap in the collection view. I have a collectionView that has fade-in animation when the button tapped and the image changes. I also tried to use reloadItem, but it still flickering.

I found that the problem is caused because I call reloadData inside the cell updating, but I could not found how to do it correctly when the button is pressed.

ViewController


import UIKit
import CoreData


class WordsVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
  
    
  
    @IBOutlet weak var wordsCategoryCollection: UICollectionView!
    

    override func viewDidLoad() {
        super.viewDidLoad()

        wordsCategoryCollection.delegate = self
        wordsCategoryCollection.dataSource = self
        wordsCategoryCollection.isScrollEnabled = false
        wordsCategoryCollection.backgroundColor = nil
        
        roundCounter = 1
    }

    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return words.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "WordsCategoryCell", for: indexPath) as? WordsCategoryCell {
            cell.backgroundColor = UIColor.clear
            
            
            cell.wordsBtn.tag = indexPath.row
            cell.wordsBtn.addTarget(self, action: #selector(changeCategoryStatus), for: .touchUpInside)
            cell.updateCell(word: words[indexPath.row])
            return cell
        }
        return UICollectionViewCell()
    }
    
    @objc func changeCategoryStatus(_ sender: UIButton!) {
        debugPrint(sender.tag)
        if words[sender.tag].categoryIsEnable == true {
                words[sender.tag].categoryIsEnable = false
                words[sender.tag].categoryImageName.removeLast()
                words[sender.tag].categoryImageName.append("F")
              } else {
                words[sender.tag].categoryIsEnable = true
                words[sender.tag].categoryImageName.removeLast()
                words[sender.tag].categoryImageName.append("T")
              }
        
        debugPrint("\(words[0].categoryImageName) - \(words[0].categoryIsEnable)")
        debugPrint("\(words[1].categoryImageName) - \(words[1].categoryIsEnable)")
        debugPrint("\(words[2].categoryImageName) - \(words[2].categoryIsEnable)")
        wordsCategoryCollection.reloadData()
        //wordsCategoryCollection.reloadItems(at: [IndexPath(row: sender.tag, section: 0)])
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        getWordsForTheGame()
    }
    
    @IBAction func playBtn(_ sender: Any) {
        performSegue(withIdentifier: "MainVC", sender: nil)
    }
    
}


Cell

class WordsCategoryCell: UICollectionViewCell {

    @IBOutlet weak var wordsBtn: UIButton!
    
    func updateCell(word: word) {
        let image = UIImage(named: word.categoryImageName)
        wordsBtn.setTitle(nil, for: .normal)
        wordsBtn.setImage(image, for: .normal)
    }
    
}

GIF


Solution

  • I'd suggest that you don't use reloadData() for what you're trying to achieve. instead I would recommend this:

    func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
            collectionView.indexPathsForSelectedItems?.forEach({ collectionView.deselectItem(at: $0, animated: false) })
            //this function will make it so only one cell can be selected at a time
            //don't add this if you want multiple cells to be selected at the same time.
            return true
        }
        
        func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
            self.checkNumberOfAnswers()
            let cell = collectionView.cellForItem(at: indexPath) as! WordsCategoryCell
            //animate the cell that is getting unselected here
        }
        
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            self.checkNumberOfAnswers()
            let cell = collectionView.cellForItem(at: indexPath) as! WordsCategoryCell
            //animate the cell that is getting selected here
        }
    

    You can animate the cell you are selecting this way the way you want or even change its data since you have access to the cell, and the same thing for the cell that is getting unselected.

    just override isSelected inside your cell and set it to do what you want when the cell is selected and when unselected.

    public override var isSelected: Bool {
            didSet {
                switch self.isSelected {
                case true:
                    //do stuff when selected
                case false:
                    //do stuff when unselected
                }
            }
        }
    

    EDIT:

    reloadData() recreates all cells that are currently loaded (in view since you're dequeuing) using all the code inside func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell, it's not efficient for this particular use case since it's running extra code again, if it's already in view and it's loaded using your configurations then there is no need to set background color to clear, set button tag, etc... of every cell again. that small animation you see happening to the other cells might be a side effect of the cell being created again in front of your eyes. Instead of changing the data inside your words array it's better to change data inside the cells that are already created.

    reloadData() is better used to actually reload new data, like for example when you make a request from a server and you get a new array to update your cells with, you put a didSet on that array and when didSet runs it will reloadData, recreating the cells with the actual new info/data.

    As for how to do it with the button, first I recommend that you add your @objc method inside your cell to improve readability since that is where your button exists, and using a delegate to get notified of it inside your ViewController.

    Secondly, you need indexPath to access the cells, not indexPath.row, so create an optional variable inside your cells called indexPath of type IndexPath and set it to indexPath when your cells are being created.

    cell.indexPath = indexPath
    

    Now, create a delegate inside your cell that has a function that passes the indexPath when your button is pressed, for example:

    protocol categoryCellDelegate {
        func buttonPressed(index: IndexPath)       
    }
    

    get the indexPath inside your VC by conforming to the protocol and when the function is called inside your VC:

    func buttonPressed(index: IndexPath) {
          let cell = wordsCategoryCollection.cellForItem(at: index) as! WordsCategoryCell
        //now do anything your want with this cell like change image, color, etc...
    }
    

    And for why I recommended using the CollectionView's didSelectItemAt is mostly cause it already provides all these and more for you.

    I used to do it using a button till a month ago (I've been coding Swift for 8 months now) until I wanted to create a quiz system where only one cell in a given section could be selected at a time, and the headache involved with making sure the previous cell get's unselected and new one is selected was just not worth it. so I used Collections View's own selection method, saw how easy it was and that it gave me all that I wanted and started using it more since.