'Connect UITableviewCell with UITableview using Combine repeat values

I'm learning combine and I want to use combine instead a delegate between cell and tableview. I have managed to connect and receive the information, but the problem is when the cell is reused, every time I generate the same event, I receive it as many times as it has been used previously in that reused cell.

I have declared cancelables in the view controller as

var cancellables: Set<AnyCancellable> = []

And this is the cellForRow method

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: MyCell.celdaReuseIdentifier, for: indexPath)
        as? MyCell else {
            return MyCell()
    }
    
    cell.index = indexPath
    cell.lbTitle.text = String("Cell \(indexPath.row)")
    
    
    cell.tapButton.compactMap{$0}
        .sink { index in
        print("tap button in cell \(index.row)")
    }.store(in: &cancellables)
    
    return cell
}

and the cell is

class MyCell: UITableViewCell {
static let cellNibName = "MyCell"
static let celdaReuseIdentifier = "MyCellReuseIdentifier"

@IBOutlet weak var lbTitle: UILabel!
@IBOutlet weak var button: UIButton!

var index: IndexPath?

let tapButton = PassthroughSubject<IndexPath?, Never>()

override func awakeFromNib() {
    super.awakeFromNib()
    // Initialization code
}

@IBAction func tapButton(_ sender: Any) {
    self.tapButton.send(index)
}
}

Thanks for your help



Solution 1:[1]

To solve your problem with reused cells you must add the Set<AnyCancellable> to the cell.

If you are only going to use an event inside cells you can use a single AnyCancellable:

Single Event (AnyCancellable)

Declares a variable in the cell of AnyCancellable Type. Every time the cell is reused a new publisher will be added replacing the previous one and you will not receive the event multiple times.

  • Cell

      class MyCell: UITableViewCell {
          static let cellNibName = "MyCell"
          static let celdaReuseIdentifier = "MyCellReuseIdentifier"
    
          @IBOutlet weak var lbTitle: UILabel!
          @IBOutlet weak var button: UIButton!
    
          var index: IndexPath?
    
          // store publisher here
          var cancellable: AnyCancellable? 
    
          // Single Publisher per cell
          let tapButton = PassthroughSubject<IndexPath?, Never>()
    
          override func awakeFromNib() {
              super.awakeFromNib()
          }
    
          @IBAction func tapButton(_ sender: Any) {
              self.tapButton.send(index)
          }
      }
    
  • ViewController

In the Viewcontroller you just have to add the publisher to the cancellable.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    guard let cell = tableView.dequeueReusableCell(withIdentifier: MyCell.celdaReuseIdentifier, for: indexPath)
    as? MyCell else {
        return MyCell()
    }

    cell.index = indexPath
    cell.lbTitle.text = String("Cell \(indexPath.row)")

    // Add your publisher to your cancellable and remove store function.

    cell.cancellable =  cell.tapButton.compactMap{$0} .sink { index in
        print("tap button in cell \(index.row)")
    }

    return cell
}

Multiples events (Set<AnyCancellable>)

Here it is the same but using a collection in case you want to have more events than just one.

  • Cell

Create a variable Set<AnyCancellable> to store the publishers. In this case, before reusing the cell, we will have to remove the cancellables before creating new ones.

    class MyCell: UITableViewCell {
        static let cellNibName = "MyCell"
        static let celdaReuseIdentifier = "MyCellReuseIdentifier"
    
        @IBOutlet weak var lbTitle: UILabel!
        @IBOutlet weak var button: UIButton!
    
        var cancellables: Set<AnyCancellable>?

        var index: IndexPath?
    
    // Multiple Publishers per cell
        let tapButton = PassthroughSubject<IndexPath?, Never>()
        let tapView = PassthroughSubject<UIImage, Never>()

    // Remove all suscriptions before reuse cell
        override func prepareForReuse() {
            super.prepareForReuse()
            cancellables.removeAll()
         }

        override func awakeFromNib() {
            super.awakeFromNib()
            // Initialization code
        }
    
        @IBAction func tapButton(_ sender: Any) {
            self.tapButton.send(index)
        }
    }
  • ViewController

In the Viewcontroller you just have to store the publishers.

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    guard let cell = tableView.dequeueReusableCell(withIdentifier: MyCell.celdaReuseIdentifier, for: indexPath)
        as? MyCell else {
            return MyCell()
    }
    
    cell.index = indexPath
    cell.lbTitle.text = String("Cell \(indexPath.row)")
    
    // Add your publisher to your cell“s collection of AnyCancellable
    
    cell.tapButton.compactMap{$0}
        .sink { index in
        print("tap button in cell \(index.row)")
    }.store(in: &cell.cancellables)
    
      return cell
 }

Good Luck!! ?

Solution 2:[2]

You have analyzed and described the problem perfectly. And so the cause is clear. Look at your cellForRow implementation and think about what it does: You are creating and adding a new pipeline to your cancellables every time your cellForRow runs, regardless of whether you've already added a pipeline for this instantiation of the cell.

So you need a way not to do that. Can you think of a way? Hint: attach the pipeline to the cell and vend it from there, so there is only one per cell. Your Set won't add the same pipeline twice, because it is a Set.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Magnoscg
Solution 2 matt