Reputation: 93
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")
}
}
Upvotes: 0
Views: 224
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
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