UICollectionView - Changing UI of one specific cell causes changes for every fifth cell

Hey guys. I am having an issue with one of my projects.

UICollectionViewCell in my UICollectionView has a plus button with the systemImage "plus". When clicked, its changing to systemImage "checkmark".

The issue is, clicking one button, changes UI for every fifth cell.

On top of that, animation does not finish. Alpha goes to 0, and then the image is being changed, alpha should go to 1 but never gets there. It stops somewhere between 0.7-0.8.

Here is my animation block:

Code Block
private func animateButtonView(_ viewToAnimate: UIView) {
        UIView.animate(withDuration: 0.2, animations: {viewToAnimate.alpha = 0}) { [weak self] (true) in
            guard let self = self else { return }
            switch true {
            case true:
                DispatchQueue.main.async {
self.addToFavoritesButton.setImage(SFSymbolsAsImg.checkmark, for: .normal)
}
                UIView.animate(withDuration: 0.2, animations: {viewToAnimate.alpha = 1} )
            case false:
                return
            }
        }
    }


Animation block is executed in my objective function that is called in addTarget for a button.

I've asked that question on many forums and got not much.
I'll appreciate any help. Many thanks!

Not sure, but sounds like some cell-reusing issue.

Can you show your code implementing UICollectionViewDataSource methods and the definition of the cell class?
Hey, thanks for getting back to me.

Here is my code for the DataSource

Code Block Swift
private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<SearchTweetsCell, Tweet>{ (cell, indexPath, tweet) in
            cell.set(with: tweet, user: self.user)
            cell.delegateSafari = self
        }
        dataSource = UICollectionViewDiffableDataSource<Section, Tweet>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, tweet) -> UICollectionViewCell? in
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: tweet)
        })
        dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) in
            let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SearchTweetsVCCollectionHeader.reuseId, for: indexPath) as! SearchTweetsVCCollectionHeader
            header.set(with: self.user)
            return header
        }
    }

Not sure what do you mean by the "definition of the cell class"? Thanks!

Not sure what do you mean by the "definition of the cell class"?

In your case, the definition of SearchTweetsCell.
Animation happens in @objc private func addToFavoritesTapped (Line 44), function animateButtonView(sender) (Line 64) is called (on line 45) that takes alpha to 0, then changes an image, and takes alpha back to 1.

Code Block
import UIKit
// MARK: - Protocols and Delegates
protocol SearchTweetsCellDelegates: class {
    func didRequestSafari(with urlString: String?)
}
final class SearchTweetsCell: UICollectionViewCell {
    // MARK: - Declarations
    static let reuseId          = "cell"   
    let addToFavoritesButton    = UIButton()
    var timeDateLabel           = UILabel()
    let tweetBodyLabel          = UserSearchVCTextView()
    var mediaStackView          = UIStackView()
    let seeOnlineButton         = UIButton()
    var sharesView              = CellMediaInfoView()
    var likesView               = CellMediaInfoView()
    var goSafariButton          = GoSafariButton()
    weak var delegateSafari:    SearchTweetsCellDelegates!
    var tweet:                  Tweet!
    var user:                   User!
    var urlString:            String?
    // MARK: - Initialise & Override
    override init(frame: CGRect) {
        super.init(frame: frame)
        configureCell()
        configureUIElements()
        layoutUI()
        configureMediaStackView()
        configureGoSafariButton()
    }
    override func prepareForReuse() { self.goSafariButton.isHidden = false }
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    // MARK: - @Objectives
    @objc private func addToFavoritesTapped(sender: UIView) {
        animateButtonView(sender)
        let favorite = fromTweetToFavoriteTweet(user: user, tweet: tweet)
        PersistenceManager.updateWithTweets(favoriteTweet: favorite, persistenceAction: .add) { [weak self] (error) in
            guard self != nil else { return }
            guard let error = error else {
                print("sucess")
                return
            }
            print(error.rawValue)
        }
        self.addToFavoritesButton.isEnabled = false
    }
    @objc private func didTapGoSafariButton(_ sender: UIView) {
        delegateSafari.didRequestSafari(with: urlString)
    }
    // MARK: - Animations
    private func animateButtonView(_ viewToAnimate: UIView) {
        UIView.animate(withDuration: 0.2, animations: {viewToAnimate.alpha = 0}) { [weak self] (true) in
            guard let self = self else { return }
            switch true {
            case true:
                DispatchQueue.main.async { self.addToFavoritesButton.setImage(SFSymbolsAsImg.checkmark, for: .normal) }
                UIView.animate(withDuration: 0.2, animations: {viewToAnimate.alpha = 1} )
            case false:
                return
            }
        }
    }
    // MARK: - Called outside 
    func set(with usersTweet: Tweet, user: User) {
        self.user                               = user
        tweet                                   = usersTweet
        urlString                               = usersTweet.urlToExpandWithSafari
        tweetBodyLabel.text                     = usersTweet.tweetText
        timeDateLabel.text                      = usersTweet.createdAt.formatToTwitterPostDate()
        sharesView.set(itemInfoType: .shares,   with: usersTweet.retweetCounter.convertToKMFormatStr())
        likesView.set(itemInfoType: .likes,     with: usersTweet.likesCounter.convertToKMFormatStr())
        if tweet.urlToExpandWithSafari == nil { goSafariButton.isEnabled = true }
        self.goSafariButton.setTitle(TweetStrings.seeFull, for: .normal)
    }
    // MARK: - Cell Configuration
    private func fromTweetToFavoriteTweet(user: User ,tweet: Tweet) -> FavoriteTweet {
        let favoriteTweet = FavoriteTweet(twitsId: tweet.twitsId,
                                          name: user.name,
                                          tweetText: tweet.tweetText,
                                          profileImageUrl: user.profileImageUrl,
                                          urlToExpandWithSafari: tweet.urlToExpandWithSafari,
                                          likesCounter: tweet.likesCounter,
                                          retweetCounter: tweet.retweetCounter,
                                          createdAt: tweet.createdAt)
        return favoriteTweet
    }
    private func configureCell() {
        backgroundColor                     = .secondarySystemBackground
        layer.cornerRadius                  = 15
    }
    private func configureGoSafariButton() {
        goSafariButton.addTarget(self, action: #selector(didTapGoSafariButton), for: .touchUpInside)
    }
    private func configureMediaStackView() {
        mediaStackView.axis                 = .horizontal
        mediaStackView.distribution         = .equalSpacing
        mediaStackView.alignment            = .center
        mediaStackView.addArrangedSubview(sharesView)
        mediaStackView.addArrangedSubview(likesView)
    }
    private func configureUIElements() {
        timeDateLabel.textColor             = .systemGray
        timeDateLabel.textAlignment         = .center     
        addToFavoritesButton.setImage(SFSymbolsAsImg.plus, for: .normal)
        addToFavoritesButton.tintColor      = ColorsTwitter.twitterBlue
        addToFavoritesButton.addTarget(self, action: #selector(addToFavoritesTapped), for: .touchUpInside)
    }
    // MARK: - Layout Configuration
    private func layoutUI() {
        addSubviews(addToFavoritesButton, timeDateLabel, tweetBodyLabel, mediaStackView, goSafariButton)
        tamicToFalse(addToFavoritesButton, mediaStackView, timeDateLabel)
        let mediaLeadingPadding: CGFloat = DeviceTypes.isiPhoneSE || DeviceTypes.isiPhone8Zoomed ? 10 : 30
        let mediaWidthMltp: CGFloat = DeviceTypes.isiPhoneSE || DeviceTypes.isiPhone8Zoomed ? 0.55 : 0.5
        NSLayoutConstraint.activate([ ... ]) // Removed from the post for less text
  }
}


Thanks for showing your code.
At a glance I cannot find any suspicious codings which may cause changes UI for every fifth cell.

I will take some time to run your code. Any additional info which helps testing your code would be welcome.


Hi. I am happy to send you the whole project and any additional info that you may find valuable if you could just find me on LinkedIn (Jakub Gawecki, curly hair on a profile image) or Facebok (Kuba Gawecki, profile image with the dog). Should be easy, not too many people with my name.
Thanks for your kind offer, but I do not want to use sort of private things to exchange development info.
If you can think of some other ways, please tell me. I will spend some time to fill the missing parts of the shown code until then.
With filling missing parts with simplified components, I could not reproduce the issue changes UI for every fifth cell.
But could find one thing (in a limited context, similar to every fifth cell).
You are not resetting the state of addToFavoritesButton:
Code Block
override func prepareForReuse() {
self.goSafariButton.isHidden = false
addToFavoritesButton.isEnabled = true
addToFavoritesButton.setImage(SFSymbolsAsImg.plus, for: .normal)
}


I guess you need to add some info into your Tweet struct (or somewhere else but retrievable from Tweet) and set the state of addToFavoritesButton in set(with:user:) according to it.


On top of that, animation does not finish. Alpha goes to 0, and then the image is being changed, alpha should go to 1 but never gets there. It stops somewhere between 0.7-0.8.

Isn't it just a result of making the button disabled?
Code Block
        self.addToFavoritesButton.isEnabled = false

Whether you use alpha-animation or not, disabled buttons show system image as if alpha being somewhere between 0.7-0.8.
Does the UIView that you pass to the animateButtonView function contain the self.addToFavoritesButton? If that is the case, you should get the pointer to it from the UIView you pass instead of the local variable. This would cause a cell reuse problem if that view is in your collection view.
So what you suggesting is to, for example:
Add var favorite: Bool parameter to my struct.
When the data of a tweet is fetched from JSON, set favorite's default value to false.
When the button addToFavorite is pushed, toggle favorites value to true and save that object with some persistence manager,
Then, every time data is fetched again for the same user to get his/her tweets, do a check if the tweet exists in persistence manager with the favorite parameter set to true, and if it does, set its UI accordingly?

I hope it makes sense what I just wrote.

Add var favorite: Bool parameter to my struct. 
When the data of a tweet is fetched from JSON, set favorite's default value to false. 
When the button addToFavorite is pushed, toggle favorites value to true and save that object with some persistence manager,
Then, every time data is fetched again for the same user to get his/her tweets, do a check if the tweet exists in persistence manager with the favorite parameter set to true, and if it does, set its UI accordingly?

Seems to be a possible implementation.
UICollectionView - Changing UI of one specific cell causes changes for every fifth cell
 
 
Q