'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 |
