2

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.

  • The issue is because of the cell reusability, you need to somehow keep track of the liked rows and then accordingly load the cells. Check this for common mistakes in tableview https://thomashanning.com/the-most-common-mistake-in-using-uitableview/ – Yaseen Majeed Apr 22 '21 at 09:48

2 Answers2

0

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
}

}

Yasheed Mohammed
  • 187
  • 1
  • 14
  • This is the answer, thank you. Can you explain to me what and how exactly it is happening step by step. Just so I can become more educated instead of me just copying and pasting :) Maybe if you can add comments in the Code that you added. –  Apr 22 '21 at 18:11
  • What is the best way to locally save the cells memory for when the user exits the app and returns later. So that the same liked cells remain liked? –  Apr 22 '21 at 22:49
0

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

Andrew Bogaevskyi
  • 2,251
  • 21
  • 25
  • I can not go this route because I’m pulling data from an api and will never know how many rows I will have until data is loaded. Anyway do do it that way? –  Apr 22 '21 at 14:34
  • In my code the array `cellModels` just an example. I've provided the way how to handle 'like' button. You probably need to update and extend this code. About API - once you pull data from API you have to save it into `cellModels` and call `tableView.reloadData()`. Everything else will work as it now. – Andrew Bogaevskyi Apr 23 '21 at 07:43
  • Does this code work for when I use UISearchBar and have the correct cell with the correct like button State? –  Apr 26 '21 at 19:00
  • Yes, the correct like button state will work as-is. – Andrew Bogaevskyi Apr 27 '21 at 07:34