Update Specific Button Image in UITableView Cell

1.5k Views Asked by At

I am trying to add an action to my "like" button. So that when the user taps the heart UIButton in a cell, the heart in the cell they tapped updates to a pink heart showing that they liked it. But instead it likes the heart they tapped and another random heart in a different cell that they did not interact with. I have been on this all day and any help would be grateful. For Example, if I like/tap my heart UIButton the buttons image I tapped updates, but when I scroll down another random heart updates from that same first cell button tap.

Also When I scroll and the cell leaves view and scroll back up the image returns back to unlike and other like buttons become liked.

2

There are 2 best solutions below

2
On

Keep a data model for your buttons state

Try with the below code

struct TableModel {
    var isLiked: Bool
}

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

var dataSource: [TableModel] = []

@IBOutlet var tableView: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()
    overrideUserInterfaceStyle = .light
    dataSource = Array(repeating: TableModel(isLiked: false), count: 20)
        self.tableView.delegate = self
        self.tableView.dataSource = self
        self.tableView.showsVerticalScrollIndicator = false
        self.tableView.reloadData()
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    dataSource.count
}

@objc func buttonSelected(_ sender: UIButton) {
    
    dataSource[sender.tag].isLiked = !dataSource[sender.tag].isLiked
    let indexPath = IndexPath(row: sender.tag, section: 0)
    tableView.reloadRows(at: [indexPath], with: .automatic)
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
    
    cell.likeBtn.tag = indexPath.row
    cell.likeBtn.addTarget(self, action: #selector(buttonSelected(_:)), for: .touchUpInside)
    let isLiked = dataSource[indexPath.row].isLiked
    if isLiked {
        cell.likeBtn.setImage(UIImage(named: "liked"), for: UIControl.State.normal)
    } else {
        //set unlike image
    }
    return cell
}

}

4
On

Currently, you have a hardcoded number of rows, but anyway you will need to have a data source with data models. When you press the button, you have to save the state of the button of a specific row. I would recommend you create a model first.

Here I provided an easy (but flexible enough) way how to do this. I haven't debugged it, but it should work and you can see the idea. I hope this would be helpful.

Create Cell Model

struct CellViewModel {
    let title: String
    var isLiked: Bool
    // Add other properties you need for the cell, image, etc.
}

Update cell class

It's better to handle top action right in the cell class. To handle this action on the controller you can closure or delegate like I did.

// Create a delegate protocol
protocol TableViewCellDelegate: AnyObject {
    func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell)
}

class TableViewCell: UITableViewCell {
    // add a delegate property
    weak var delegate: TableViewCellDelegate?
    
    @IBOutlet var titleTxt: UILabel!
    @IBOutlet var likeBtn: UIButton!
    //...
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // You can add target here or an action in the Storyboard/Xib
        likeBtn.addTarget(self, action: #selector(likeButtonSelected), for: .touchUpInside)
    }

    /// Method to update state of the cell
    func update(with model: CellViewModel) {
        titleTxt.text = model.title
        likeBtn.isSelected = model.isLiked
        // To use `isSelected` you need to set different images for normal state and for selected state
    }
        
    @objc private func likeButtonSelected(_ sender: UIButton) {
        sender.isSelected.toggle()
        delegate?.didSelectLikeButton(isLiked: sender.isSelected, forCell: self)
    }
}

Add an array of models and use it

This is an updated class of ViewController with usage of models.

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    // Provide a list of all models (cells)
    private var cellModels: [CellViewModel] = [
        CellViewModel(title: "Title 1", isLiked: false),
        CellViewModel(title: "Title 2", isLiked: true),
        CellViewModel(title: "Title 3", isLiked: false)
    ]
    
    @IBOutlet var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        overrideUserInterfaceStyle = .light
        
        self.tableView.delegate = self
        self.tableView.dataSource = self
        self.tableView.showsVerticalScrollIndicator = false
        self.tableView.reloadData()
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // return count of cell models
        return cellModels.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
        
        let model = cellModels[indexPath.row]

        // call a single method to update the cell UI
        cell.update(with: model)
         
        // and you need to set delegate in order to handle the like button selection
        cell.delegate = self
        
        return cell
    }
}

extension ViewController: TableViewCellDelegate {
    func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell) {
        // get an indexPath of the cell which call this method
        guard let indexPath = tableView.indexPath(for: cell) else {
            return
        }

        // get the model by row
        var model = cellModels[indexPath.row]

        // save the updated state of the button into the cell model
        model.isLiked = isLiked
        
        // and set the model back to the array, since we use struct
        cellModels[indexPath.row] = model
    }
}