0

I'm trying to set an image for a button's normal state which is located in a collectionView cell. When the button is pressed the image changes. The problem is every four cells it repeats the same image as the original cell when the button is pressed. Is there a way to not have it repeat itself and when the button is pressed its only for that individual cell?

Here is the code:

class FavoritesCell: UICollectionViewCell {

  var isFavorite: Bool = false

  @IBOutlet weak var favoritesButton: UIButton!

  @IBAction func favoritesButtonPressed(_ sender: UIButton) {
        _ = self.isFavorite ? (self.isFavorite = false, self.favoritesButton.setImage(UIImage(named: "favUnselected"), for: .normal)) : (self.isFavorite = true, self.favoritesButton.setImage(UIImage(named: "favSelected"), for: .selected))

    }
}

I've tried doing this but for some strange reason the 'selected' state image is never shown even when the button is pressed:

let button = UIButton()

override func awakeFromNib() {
    super.awakeFromNib()

    button.setImage(UIImage(named: "favUnselected"), for: .normal)
    button.setImage(UIImage(named: "favSelected"), for: .selected)
}
Rakesha Shastri
  • 10,127
  • 3
  • 29
  • 42
SwiftyJD
  • 4,091
  • 6
  • 33
  • 79
  • 2
    Cells are reused. Reset the state in `cellForItemAt`. – rmaddy Aug 30 '18 at 15:43
  • @rmaddy the thing is I want it to save the image for uibutton normal state if it changed and if the user scrolls back to original cell. – SwiftyJD Aug 30 '18 at 15:45
  • Exactly. That's why you need to keep the state stored in your data model and reset the cell accordingly each time in `cellForItemAt`. – rmaddy Aug 30 '18 at 15:46
  • @rmaddy I'm not completely sure how to do that, when I print the state after button is pressed I get : UIControlState(rawValue: 1) – SwiftyJD Aug 30 '18 at 15:49
  • While creating cell in cellForRowAt methods make sure button of which cell should be selected. for each cell. – bestiosdeveloper Aug 30 '18 at 15:54

2 Answers2

0

The cell is most likely reused and your isFavorite is set to true.

Just try adding

func prepareForReuse() {
    super.prepareForReuse()
    self.isFavorite = false
}

This will set the button to original image when cell is to be reused.

Also since you have your button have two states for selected why do this dance

  _ = self.isFavorite ? (self.isFavorite = false, self.favoritesButton.setImage(UIImage(named: "favUnselected"), for: .normal)) : (self.isFavorite = true, self.favoritesButton.setImage(UIImage(named: "favSelected"), for: .selected))

where you could only say self.favoritesButton.selected = self.isFavorite

Change your cell code to:

class FavoritesCell: UICollectionViewCell {
    @IBOutlet weak var favoritesButton: UIButton!

    var isFavorite: Bool = false {
        didSet {
            favoritesButton.selected = isFavorite
        }
    }

    @IBAction func favoritesButtonPressed(_ sender: UIButton) {
        favoritesButton.selected = !favoritesButton.selected       
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        isFavorite = false
    }
}
Ladislav
  • 6,767
  • 5
  • 24
  • 29
  • I just tried this, its still repeating for some strange reason – SwiftyJD Aug 30 '18 at 16:03
  • This works as far as having the other cells unselected when selecting a cell, but when scrolling to first cell that was selected, it doesn't stay selected, it's now unselected as well. – SwiftyJD Aug 30 '18 at 16:13
  • Then you need to keep track of that not in the cell but in the `ViewController` that is presenting the `UICollectionView` basically have like an array of IndexPaths that are favourited and then you have to configure the cell with the correct state when you do `cellForItemAt:` – Ladislav Aug 30 '18 at 16:16
0

Every time your cell is dequeued cellForItemAt is called. This is the place where you configure your cell data. So if you need to show cell marked as favourite, you can do it here.

So how do you do it there? Let's say all your cells are not selected in the beginning. Fine. You don't have to say anything in cellForItemAt. Now let's say you mark a few cells as favourite. What happens here is, it will reflect the change when the cell is visible because the button is hooked to a selector which will make the changes.

Now here is the problem. When you scroll and the cell disappears, the information about your cell being marked as favourite is lost! So what you need to do, is maintain an array which will store the IndexPath of all the selected cells. (Make sure to remove the IndexPath when a cell is removed from favourite!) Let's call that array favourites. If you can use your data source for the collection view to store the selected state information that is also fine. Now you have to store the information about whether your cell is marked as favourite in your button selector.

@objc func buttonTapped() {
    if favourites.contains(indexPath) {   // Assuming you store indexPath in cell or get it from superview
        favourites.removeAll(where: {$0 == indexPath})
    } else {
        favourites.append(indexPath)
    }
}

After you have stored the information about the cell, every time you dequeue a cell, you need to check if the IndexPath is favourites. If it is, you call a method which sets the cell in the selected state.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    // Dequeue cell and populating cell with data, etc
    if favourites.contains(indexPath) {
        cell.showFavourite()
    }
}

Done? No! Now we have another problem. This problem is associated with the reuse of the cell. So what happens in cellForItemAt actually? You dequeue a cell and use it to display information. So when you dequeue it what happens is, it might have already been used for showing some other information in some other index path. So all the data that was existing there will persist. (Which is why you have the problem of favourites repeating every 4 cells!)

So how do we solve this? There is method in UICollectionViewCell which is called before a cell is dequeued - prepareCellForReuse. You need to implement this method in your cell and remove all the information from the cell, so that it is fresh when it arrives at cellForItemAt.

func prepareForReuse() {
     //Remove label text, images, button selected state, etc
}

Or you could always set every value of everything inside the cell in cellForItemAt so that every information is always overwritten with the necessary value.

Edit: OP says he has a collection view inside a collection view. You can identify which collection view is called like this,

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

    if collectionView === favoriteCollectionView { // This is the collection view which contains the cell which needs to be marked as favourite
        // Dequeue cell and populating cell with data, etc
        if favourites.contains(indexPath) {
            cell.showFavourite()
        }
        return cell
    }
    // Dequeue and return for the other collectionview
}
Rakesha Shastri
  • 10,127
  • 3
  • 29
  • 42
  • This is good but I have 2 collection view cells at once (in order to get horizontal scroll) with one cell embedded in other cell (which contains collectionView), How would I get the indexPath of the embedded cell? – SwiftyJD Aug 30 '18 at 16:52
  • @SwiftyJD if you have 2 collectionViews, you can check which collection view is being acted on in the the delegate by comparing the collection view in the delegate with your own. And you will need 2 arrays then or use their respective data sources to store the favourite information. If you have favourites in only one collectionView, then check if the collection view is your collection view in the respective delegate and call the method to mark the cell as favourite in that cell. – Rakesha Shastri Aug 30 '18 at 16:58
  • Yes, there is a collectionView inside the first cell, inside that collectionView is another collectionView cell which contains the view with the button – SwiftyJD Aug 30 '18 at 16:59
  • @SwiftyJD i suggest you share your `cellForItemAt` delegate that you have written. – Rakesha Shastri Aug 30 '18 at 17:01
  • I updated the question with both collectionview cell code – SwiftyJD Aug 30 '18 at 17:08
  • @SwiftyJD nice. Your implement delegates for both of the collection views separately, so what is the problem? Just do what i said in the relevant delegate. – Rakesha Shastri Aug 30 '18 at 17:12
  • The button itself is in the collection view cell. When it’s pressed the ‘didSelectItemAtIndexParh’ isn’t called in cell with collectionview – SwiftyJD Aug 30 '18 at 17:15
  • @SwiftyJD you need to handle it in button press, not did select – Rakesha Shastri Aug 30 '18 at 17:17
  • I see what you mean but how would you get the index path from the button press – SwiftyJD Aug 30 '18 at 17:22
  • @SwiftyJD You can get it like this. https://stackoverflow.com/a/37589164/7734643 That is an example for getting it from a tableViewCell, but the same logic applies for collectionViewCell as well. – Rakesha Shastri Aug 30 '18 at 17:23