Search code examples
iosswiftuicollectionviewuilabeluicollectionviewcell

Data From Label in CollectionViewCell Sometimes Refreshes on Reload other times it Doesn't


First let me say this seems to be a common question on SO and I've read through every post I could find from Swift to Obj-C. I tried a bunch of different things over the last 9 hrs but my problem still exists.

I have a vc (vc1) with a collectionView in it. Inside the collectionView I have a custom cell with a label and an imageView inside of it. Inside cellForItem I have a property that is also inside the the custom cell and when the property gets set from datasource[indePath.item] there is a property observer inside the cell that sets data for the label and imageView.

There is a button in vc1 that pushes on vc2, if a user chooses something from vc2 it gets passed back to vc1 via a delegate. vc2 gets popped.

The correct data always gets passed back (I checked multiple times in the debugger).

The problem is if vc1 has an existing cell in it, when the new data is added to the data source, after I reload the collectionView, the label data from that first cell now shows on the label in new cell and the data from the new cell now shows on the label from old cell.

I've tried everything from prepareToReuse to removing the label but for some reason only the cell's label data gets confused. The odd thing is sometimes the label updates correctly and other times it doesn't? The imageView ALWAYS shows the correct image and I never have any problems even when the label data is incorrect. The 2 model objects that are inside the datasource are always in their correct index position with the correct information.

What could be the problem?

vc1: UIViewController, CollectionViewDataSource & Delegate {

    var datasource = [MyModel]() // has 1 item in it from viewDidLoad

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell

        cell.priceLabel.text = ""
        cell.cleanUpElements()
        cell.myModel = dataSource[indexPath.item]

        return cell
    }

    // delegate method from vc2
    func appendNewDataFromVC2(myModel: MyModel) {

        // show spinner

        datasource.append(myModel) // now has 2 items in it

        // now that new data is added I have to make a dip to fb for some additional information
        firebaseRef.observeSingleEvent(of: .value, with: { (snapshot) in

            if let dict = snapshot.value as? [String: Any] else { }

            for myModel in self.datasource {
                myModel.someValue = dict["someValue"] as? String
            }

            // I added the gcd timer just to give the loop time to finish just to see if it made a difference
            DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {

               self.datasource.sort { return $0.postDate > $1.postDate } // Even though this sorts correctly I also tried commenting this out but no difference
               self.collectionView.reloadData()

               // I also tried to update the layout
               self.collectionView.layoutIfNeeded()

               // remove spinner
            }
        })
    }    
}

CustomCell Below. This is a much more simplified version of what's inside the myModel property observer. The data that shows in the label is dependent on other data and there are a few conditionals that determine it. Adding all of that inside cellForItem would create a bunch of code that's why I didn't update the data it in there (or add it here) and choose to do it inside the cell instead. But as I said earlier, when I check the data it is always 100% correct. The property observer always works correctly.

CustomCell: UICollectionViewCell {

    let imageView: UIImageView = {
        let iv = UIImageView()
        iv.translatesAutoresizingMaskIntoConstraints = false
        return iv
    }()

    let priceLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    var someBoolProperty = false

    var myModel: MyModel? {
        didSet {

           someBoolProperty = true

           // I read an answer that said try to update the label on the main thread but no difference. I tried with and without the DispatchQueue
           DispatchQueue.main.async { [weak self] in
                self?.priceLabel.text = myModel.price!
                self?.priceLabel.layoutIfNeeded()  // tried with and without this
           }

           let url = URL(string: myModel.urlStr!)
           imageView.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"))

           // set imageView and priceLabel anchors
           addSubview(imageView)
           addSubview(priceLabel)

           self.layoutIfNeeded() // tried with and without this
        }
    }

    override func prepareForReuse() {
        super.prepareForReuse()

        // even though Apple recommends not to clean up ui elements in here, I still tried it to no success
        priceLabel.text = ""
        priceLabel.layoutIfNeeded() // tried with and without this
        self.layoutIfNeeded() // tried with and without this

        // I also tried removing the label with and without the 3 lines above 
        for view in self.subviews {
            if view.isKind(of: UILabel.self) {
                view.removeFromSuperview()
            }
        }
    }

    func cleanUpElements() {
        priceLabel.text = ""
        imageView.image = nil
    }
}

I added 1 breakpoint for everywhere I added priceLabel.text = "" (3 total) and once the collectionView reloads the break points always get hit 6 times (3 times for the 2 objects in the datasource).The 1st time in prepareForReuse, the 2nd time in cellForItem, and the 3rd time in cleanUpElements()


Solution

  • Turns out I had to reset a property inside the cell. Even though the cells were being reused and the priceLabel.text was getting cleared, the property was still maintaining it's old bool value. Once I reset it via cellForItem the problem went away.

    10 hrs for that, smh

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
    
        cell.someBoolProperty = false
        cell.priceLabel.text = ""
        cell.cleanUpElements()
        cell.myModel = dataSource[indexPath.item]
    
        return cell
    }