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)
}
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
}
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
}
}