Mohamed Salah
Mohamed Salah

Reputation: 93

UITableView reloadRows() called by action button inside custom UITableViewCell always lags one step behind

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

Upvotes: 0

Views: 224

Answers (2)

Mohamed Salah
Mohamed Salah

Reputation: 93

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

Upvotes: 0

DonMag
DonMag

Reputation: 77423

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(...)

Upvotes: 2

Related Questions