0

What I have: I have a CollectionViewCell as .xib + .swift files. Every cell gets their data from the database. Inside every cell I have a like button.

What I want: When I press the like button of a certain cell, I want to be able to read this data so I can change it and write the new data in the Database. So I want to change the like attribute of the dataset of a certain cell and save it in the DB

What I tried: I have the indexPath of the cell but how can I read the data of the cell?

    @IBAction func likeButton(_ sender: UIButton) {

        var superview = self.superview as! UICollectionView

        let buttonPosition:CGPoint = sender.convert(CGPoint.zero, to:superview)
        let indexPath = superview.indexPathForItem(at: buttonPosition)

        print(superview.cellForItem(at: indexPath!))
        
        // Change picture
        if sender.currentImage == UIImage(systemName: "heart.fill") {
            sender.setImage(UIImage(systemName: "heart"), for: .normal)
        } else {
            sender.setImage(UIImage(systemName: "heart.fill"), for: .normal)
        }
        
    }

UICollectionViewDataSource


    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.identifier,
                                                      for: indexPath) as! MyCollectionViewCell
        
        cell.backgroundColor = .white
        
        let newShoeCell = shoesArray?.randomElement()

        // Fill cells with data
        cell.imageView.image = UIImage(named: newShoeCell!.imgName)
        cell.shoeTitle.text = newShoeCell!.title
        cell.price.text = String(newShoeCell!.price)
        cell.ratingNumberLabel.text = String(newShoeCell!.meanRating)
        cell.floatRatingView.rating = newShoeCell!.meanRating
        
        if newShoeCell!.liked {
            cell.likeButtonOutlet.setImage(UIImage(systemName: "heart.fill"), for: .normal)
        } else {
            cell.likeButtonOutlet.setImage(UIImage(systemName: "heart"), for: .normal)
        }
        
        return cell
    }
Kinuhatek
  • 85
  • 2
  • 10

2 Answers2

2

You need to change your thinking. It is not the data "of a cell" A cell is a view object. It displays information from your data model to the user, and collects input from the user.

You asked "...how can I read the data of the cell?" Short answer: Don't. You should be saving changes into your data model as you go, so once you have an index path, you should use it to index into your data model and get the data from there.

You need to figure out which IndexPath the tapped button belongs to, and fetch the data for that IndexPath from your data model.

If you look at my answer on this thread I show an extension to UITableView that lets you figure out which IndexPath contains a button. Almost the exact same appraoch should work for collection views. The idea is simple:

  • In the button action, get the coordinates of the button's frame.

  • Ask the owning table/collection view to convert those coordinates to an index path

  • Use that index path to fetch your data.

The only difference is that for collection views, the method you use to figure out which IndexPath the button maps to is indexPathForItem(at:) instead of indexPathForRow(at:)

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • Hey, thank you for your answer! But I think you have not read my code. I already got the indexPath and the question is: How can I fetch my data when I have the indexPath. I look forward to your answer. – Kinuhatek Sep 17 '20 at 19:03
  • Two points: 1. Your code assumes that your button is one level down from the UICollectionView. That's fragile. If you decide to add a stack view, group views inside other views, or otherwise change the structure of your cell, it will break. Take a look at my extension. It's much more reliable. – Duncan C Sep 17 '20 at 19:20
  • Second: As the user interacts with the different fields, you should save any changes the user makes into your model object as you go. If the user enters text into a text field, for example, and then scrolls the cell off-screen, if you haven't saved the changes to the model, they will be lost. Once you have an IndexPath, use it to index into your model object and fetch the data for that IndexPath. – Duncan C Sep 17 '20 at 19:25
  • I have implemented your extension. It works well, I get the indexPath like before but on a better way, thank you for this. But how can I now fetch the data? This was my main question ^^ – Kinuhatek Sep 17 '20 at 19:55
  • Edit your questinon to show your code that populates a cell, and the code that collects user changes and saves those changes. I can't be more specific without knowing how you've implemented your app. – Duncan C Sep 17 '20 at 19:59
  • 1
    Typically for a row and column collection view or a sectioned table view, your model object might be an array of arrays of structs, where the struct contains all the fields you display in a cell. When the user makes a change, you update the struct in that entry in yoru array. To fetch values, you'd fetch the struct from the appropriate array indexes in your model array-of-arrays. – Duncan C Sep 17 '20 at 20:01
  • I added the code that populates a cell. This are random datasets, just to test the functionality. The code that collects users changes is only the like button, nothing else. – Kinuhatek Sep 17 '20 at 20:05
  • Yes! All the data is in an array called shoesArray. This array is in the CollectionViewController but I want to access to this array from an other ViewController, called MyCollectionViewCell. MyCollectionViewCell is the ViewController of my Cell. How can I access shoesArray from MyCollectionViewCell? – Kinuhatek Sep 17 '20 at 20:13
  • You would typically only pass the data for the individual cell to the cell's view controller, and then have a delegate method (or a closure) that gets invoked if the user changes that data in the cell. (the trend is to unidirectional data flow, where you pass immutable objects around, make changes to them, and then pass the changed object back rather than sharing access to a reference object.) – Duncan C Sep 21 '20 at 13:55
0

I suggest you add a property to your cell

class MyCell: UITableViewCell {
var tapAction: (() -> Void)?
...
@IBAction func likeButton(_ sender: UIButton) {
  tapAction?()
  ...
}

}

and set it in view controller

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  ...
  cell.tapAction = { [weak self] in
  // call to database or whatever here and then reload cell
  tableView.reloadRows(at: [indexPath], with: .automatic)
  ...
}

In general, cell should never care what its indexPath is, nor should it make calls to superview

Alexandra
  • 224
  • 2
  • 5
  • Making your button invoke a closure is a good solution to the problem. A couple of comments: The OP is asking about collection views, not table views. Also, you don't need to, and should not, call reloadRows in your collection view/table view's cellForRowAt/ItemForRowAt method. That would cause an endless loop where you serve up a cell, and then tell the collection/table view to reload it, so it asks you for the cell again, ad infinitum. – Duncan C Sep 17 '20 at 16:03
  • My bad, but solution for collection view will look very much alike. Also, in this particular case there will be no endless loop, as reloading cell will not trigger call to closure. – Alexandra Sep 17 '20 at 16:05
  • Oh, I misread your code. Your call to reload is in the button closure. My mistake. – Duncan C Sep 17 '20 at 16:27