0

I have a UITableView and I made a custom cellview to add a button inside each cell, the button is supposed to change its color when clicked. Although the data is updated and printed correctly, the view always lags one step behind i.e. when I click the first button it doesn't change its color until I click the next one.I suspect that the reloadRows() function causes this problem when called from inside the tableview Cell.

import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var mlist = [["1","2","3"], ["4","5","6","7","8"],["9","10"],["11","12"],["13","14"]]
    var leagues = ["LaLiga", "Premier League", "Bundesliga", "Serie A", "Ligue 1"]
    var hidden = Set<Int>()
    @IBOutlet weak var tbl: UITableView!
    
    var fv = Set<IndexPath>()
    
    func indxs(_ section:Int) -> [IndexPath] {
        var indxs = [IndexPath]()
        for row in 0..<mlist[section].count {
            indxs.append(IndexPath(row: row, section: section))
        }
        return indxs
    }
    
    @objc
    private func hideSection(sender: UIButton) {
        let section = sender.tag
        if hidden.contains(section) {
            hidden.remove(section)
            tbl.insertRows(at: indxs(section), with: .fade)
            
        }else{
            hidden.insert(section)
            tbl.deleteRows(at: indxs(section), with: .fade)
        }
    }
    
    func cellMethod(cell: UITableViewCell) {
        guard let i = tbl.indexPath(for: cell) else { return }
        if fv.contains(i){fv.remove(i)}else{fv.insert(i)}
        tbl.reloadRows(at: [i], with: .none)
    }

    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if hidden.contains(section) {
            return 0
        }
        return mlist[section].count
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return mlist.count
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let sectionButton = UIButton()
        sectionButton.setTitle(leagues[section], for: .normal)
        sectionButton.backgroundColor = .purple
        sectionButton.tag = section
        sectionButton.addTarget(self, action: #selector(self.hideSection(sender:)), for: .touchUpInside)
        return sectionButton
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath) as! mcell
        cell.link = self
        cell.textLabel?.text = mlist[indexPath.section][indexPath.row]
        if fv.contains(indexPath){
            cell.accessoryView?.tintColor = .orange
        }else{
            cell.accessoryView?.tintColor = .gray
        }
        return cell
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        tbl.register(mcell.self, forCellReuseIdentifier: "cell1")
    }
}
import UIKit

class mcell: UITableViewCell{
    var link:ViewController?
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        let starButton = UIButton(type: .system)
        starButton.setImage(#imageLiteral(resourceName: "fav_star"), for: .normal)
        starButton.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        starButton.tintColor = .red
        starButton.addTarget(self, action: #selector(handleMarkAsFavorite), for: .touchUpInside)
        accessoryView = starButton
    }
    
    @objc private func handleMarkAsFavorite() {
        print(self.textLabel!.text!)
        link?.cellMethod(cell: self)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

enter image description here

2 Answers2

2

Giving your cell a reference to its controller is a bad pattern.

You're much better off using a closure to let the cell "call back" to the controller when you tap the star.

Here's an update to your cell class:

class mcell: UITableViewCell{
    
    // "callback" closure to tell the controller that the Star was tapped
    var starWasTapped: (() -> ())?
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        let starButton = UIButton(type: .system)
        starButton.setImage(#imageLiteral(resourceName: "fav_star"), for: .normal)
        starButton.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        starButton.tintColor = .red
        starButton.addTarget(self, action: #selector(handleMarkAsFavorite), for: .touchUpInside)
        accessoryView = starButton
    }
    
    @objc private func handleMarkAsFavorite() {
        print(self.textLabel!.text!)
        
        // tell the controller the Star was tapped
        starWasTapped?()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

Then, your cellForRowAt will look like this:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath) as! mcell
    
    cell.textLabel?.text = mlist[indexPath.section][indexPath.row]
    if fv.contains(indexPath){
        cell.accessoryView?.tintColor = .orange
    }else{
        cell.accessoryView?.tintColor = .gray
    }

    // set the cell's "callback" closure
    cell.starWasTapped = { [weak self] in
        guard let self = self else { return }
        if self.fv.contains(indexPath){self.fv.remove(indexPath)}else{self.fv.insert(indexPath)}
        self.tbl.reloadRows(at: [indexPath], with: .none)
    }

    return cell
}

and now you have no need for the separate func cellMethod(...)

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Unfortunately this did not solve the problem, but thank you for this nice closure trick – Mohamed Salah Jan 16 '21 at 22:17
  • hmm... it works for me. Did you use the exact code I posted? – DonMag Jan 16 '21 at 22:25
  • yes i did.. the same problem happens.. the animation change is always lagging one step behind.. i dont know may be it's a simulator problem, but in other situation when i assign the action to the whole cell by using reloadRows() inside didSelectRow() delegate function it works fine, but i need the action to be assigned to the button not to the whole cell – Mohamed Salah Jan 16 '21 at 23:49
  • i just found a solution here and it worked for me :) https://stackoverflow.com/questions/24787098/programmatically-emulate-the-selection-in-uitableviewcontroller-in-swift , by adding action to the button to force triggering the delegate function (didSelectRowAtIndexPath) and inside the delegate function i apply the reloadRows() – Mohamed Salah Jan 17 '21 at 00:12
0

I found a solution here and it worked for me https://stackoverflow.com/a/39416618/14061160 , by adding action to the button to force triggering the delegate function (didSelectRowAtIndexPath) and inside the delegate function I apply reloadRows()

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath) as! mcell
//        cell.link = self
        cell.textLabel?.text = mlist[indexPath.section][indexPath.row]
        if fv.contains(indexPath){
            cell.accessoryView?.tintColor = .orange
        }else{
            cell.accessoryView?.tintColor = .gray
        }
        cell.starWasTapped = { [weak self] in
            guard let self = self else { return }
            if self.fv.contains(indexPath){self.fv.remove(indexPath)}else{self.fv.insert(indexPath)}
            self.tbl.delegate!.tableView?(self.tbl, didSelectRowAt: indexPath)
        }
        return cell
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tbl.reloadRows(at: [indexPath], with: .fade)
    }