tableview.reloadRows() takes three hit to reload

I am trying to update my array data and reload the tableview once user click on a cell, however, it takes three hits till the tableview reload a specific row that was selected. This problem only occur when I use (tableview.reloadRows()) but if I use (tableview.reloadData()) everything is working fine with just a single click but I don't want to reload the entire tableview. Can someone please tell me what I did wrong?
Code Block
extension ViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dummyData.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let todoCell = tableView.dequeueReusableCell(withIdentifier: "todoCell") as! TodoTableViewCell
        todoCell.dataLabel.text = dummyData[indexPath.row].item
        if (dummyData[indexPath.row].done == false) {
            todoCell.dataLabel.hideStrikeTextLayer()
        }else{
            todoCell.dataLabel.strikeThroughText()
        }
        todoCell.delegate = self
        return todoCell
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let index = IndexPath(row: indexPath.row, section: 0)
        if dummyData[indexPath.row].done == false {
            dummyData[indexPath.row].done = true
            todoTableView.reloadRows(at: [index], with: .none)     
        }else {
            dummyData[indexPath.row].done = false
            todoTableView.reloadRows(at: [index], with: .none)
        }
    }
}


I cannot reproduce the issue you described with the shown code filling missing parts by guess.

Can you show enough code to reproduce the issue?
A possible problem is that, when you resect an already selected cell, didSelect is not called.
Just a deselect is called.

Did you allow for multiple selections ?
So, you should implement
Code Block
func tableView(UITableView, didDeselectRowAt: IndexPath)

as well
How is dummyData[indexPath.row].done set at first ? I suppose all values gave done == false.
Please show how you declared and initialized.

Why do you recompute indexPath ?
Code Block
todoTableView.reloadRows(at: [indexPath], with: .none)

Should be enough (I assume there is only one section)

Note: There is no need to reset the cell delegate, but that should not be the problem.

@Claude31 Hi, thank you so much for your suggestions.
  • I have tried to implement the deselectRowAt() and allow multiple selection for my tableview but things still the same. Like I said, implementing just the didSelectRowAt() work perfectly with tableview.reloadData() but when it comes to tableView.reloadRowsAt() it takes a minimum of two clicks to reload the row.

  • My dummyData was set to false before user click on the row, I just don't want to paste everything here because it is too long so I just paste the necessary part only.

  • May you please let me know what do you mean by recompute indexPath? In this part I just use the indexPath from the didSelectRowsAt() method without making any changes to the indexPath.

  • May you let me know what you mean by "There is no need to reset the cell delegate?"

@OOPer Hi, thank you so much for your comment.

Actually the code is so long that I cannot paste the rest of it here so I just paste the necessary part here. What I am trying to do is that I try to reload the row of my tableview whenever user click on a specific row by doing all the logic inside the didSelectRowAt() method as I have stated above.


so I just paste the necessary part here.

Sorry, but it was less than necessary.

What I am trying to do is that I try to reload the row of my tableview whenever user click on a specific row by doing all the logic inside the didSelectRowAt() method as I have stated above.

Please show enough code to reproduce the issue. As I wrote I cannot reproduce the issue with your shown code.

May you please let me know what do you mean by recompute indexPath? In this part I just use the indexPath from the didSelectRowsAt() method without making any changes to the indexPath.

Code Block
        let index = IndexPath(row: indexPath.row, section: 0)

You could just use indexPath that is passed as parameter.

May you let me know what you mean by "There is no need to reset the cell delegate?"

Code Block
        todoCell.delegate = self

@OOPer
Code Block language
//
//  ViewController.swift
//  Todoist
//
//  Created by David Im on 1/14/21.
//
import UIKit
import SwipeCellKit
import StrikethroughLabel
class ViewController: UIViewController {
    private var todoTableView   = UITableView()
    private var searchBar       = UISearchBar()
    
    private var dummyData = [Data(item: "Go to shopping", done: false),
                             Data(item: "Go to school", done: false),
                             Data(item: "Buy milk", done: false),
                             Data(item: "Do homework", done: false),
                             Data(item: "Clean room", done: false),
                             Data(item: "Wash car", done: false)]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        title = "Todoist"
        
        setupViews()
    }
    
    private func setupViews() {
        view.addSubview(todoTableView)
        view.addSubview(searchBar)
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addItem))
        
        searchBar.translatesAutoresizingMaskIntoConstraints                                                         = false
        searchBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive   = true
        searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive           = true
        searchBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
        
        todoTableView.delegate          = self
        todoTableView.dataSource        = self
        todoTableView.register(TodoTableViewCell.self, forCellReuseIdentifier: "todoCell")
        
        //todoTableView.allowsSelection   = true
        todoTableView.rowHeight           = 70
        //todoTableView.separatorStyle    = .none
            
        todoTableView.translatesAutoresizingMaskIntoConstraints                                                     = false
        todoTableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 0).isActive                   = true
        todoTableView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0).isActive     = true
        todoTableView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0).isActive   = true
        todoTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
    }
    
    @objc private func addItem() {
        
        var dataFromTextField:UITextField?
        var addButton:UIAlertAction!
        
        let addItemBox = UIAlertController(title: "Add Item", message: .none, preferredStyle: .alert)
        addItemBox.addTextField { (item) in
            dataFromTextField = item
        }
        addButton = UIAlertAction(title: "Add", style: .cancel) { (action) in
            if let data = dataFromTextField?.text, !data.isEmpty {
                self.dummyData.append(Data(item: data, done: false))
            }else{
                self.dismiss(animated: true, completion: nil)
            }
            DispatchQueue.main.async {
                self.todoTableView.reloadData()
            }
        }
        addItemBox.addAction(addButton)
        present(addItemBox, animated: true, completion: nil)
    }
}
extension ViewController: UITableViewDataSource, UITableViewDelegate, SwipeTableViewCellDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dummyData.count
        
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let todoCell = tableView.dequeueReusableCell(withIdentifier: "todoCell") as! TodoTableViewCell
        todoCell.dataLabel.text = dummyData[indexPath.row].item
        if (dummyData[indexPath.row].done == false) {
            todoCell.dataLabel.hideStrikeTextLayer()
        }else{
            todoCell.dataLabel.strikeThroughText()
        }
        todoCell.delegate = self
        return todoCell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let index = IndexPath(row: indexPath.row, section: 0)
        if dummyData[indexPath.row].done == false {
            dummyData[indexPath.row].done = true
            todoTableView.reloadRows(at: [index], with: .none)
            
        }else {
            dummyData[indexPath.row].done = false
            todoTableView.reloadRows(at: [index], with: .none)
        }
    }
    
    
    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
        guard orientation == .right else { return nil }
        let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
            // handle action by updating model with deletion
            self.dummyData.remove(at: indexPath.row)
            DispatchQueue.main.async {
                self.todoTableView.reloadData()
            }
        }
        // customize the action appearance
        deleteAction.image = UIImage(named: "delete")
        return [deleteAction]
    }
    
    func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions {
        var options = SwipeOptions()
        options.expansionStyle = .destructive
        options.transitionStyle = .border
        return options
    }
}


@Claude I have tried to not use the recompute indexPath before but it still performing the same.

@OOPer 

Thanks for showing your new code. But unfortunately, it does not reproduce the issue.
I do not know what are SwipeCellKit and StrikethroughLabel, and filled missing parts with my own code which I guessed.
so any of the two is causing the issue.

Please try removing SwipeCellKit and see what happens.
@OOPer I don't think those are the main cause because if I use todoTableView.reloadData() it works perfectly.

 if I use todoTableView.reloadData() it works perfectly. 

That cannot be a good reason.

If you try not using SwipeCellKit and can reproduce the issue, it may not be the cause of the issue.
Could you show code for
hideStrikeTextLayer()
and
strikeThroughText()

Note that you could simplify code:
Code Block
if dummyData[indexPath.row].done == false {
dummyData[indexPath.row].done = true
todoTableView.reloadRows(at: [index], with: .none)
} else {
dummyData[indexPath.row].done = false
todoTableView.reloadRows(at: [index], with: .none)
}

with
Code Block
dummyData[indexPath.row].done.toggle()
todoTableView.reloadRows(at: [index], with: .none)


Also try to call reloadRows in a
Code Block
DispatchQueue.main.async {
}

as you did for reloadData()

When you paste code, please use Paste and Match Style, to avoid all the extra lines that make it nearly impossible to read.
Code Block
import UIKit
import SwipeCellKit
import StrikethroughLabel
class ViewController: UIViewController {
private var todoTableView = UITableView()
private var searchBar = UISearchBar()
private var dummyData = [Data(item: "Go to shopping", done: false),
Data(item: "Go to school", done: false),
Data(item: "Buy milk", done: false),
Data(item: "Do homework", done: false),
Data(item: "Clean room", done: false),
Data(item: "Wash car", done: false)]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Todoist"
setupViews()
}
private func setupViews() {
view.addSubview(todoTableView)
view.addSubview(searchBar)
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addItem))
searchBar.translatesAutoresizingMaskIntoConstraints = false
searchBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
searchBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
todoTableView.delegate = self
todoTableView.dataSource = self
todoTableView.register(TodoTableViewCell.self, forCellReuseIdentifier: "todoCell")
//todoTableView.allowsSelection = true
todoTableView.rowHeight = 70
//todoTableView.separatorStyle = .none
todoTableView.translatesAutoresizingMaskIntoConstraints = false
todoTableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 0).isActive = true
todoTableView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0).isActive = true
todoTableView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0).isActive = true
todoTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
}
@objc private func addItem() {
var dataFromTextField:UITextField?
var addButton:UIAlertAction!
let addItemBox = UIAlertController(title: "Add Item", message: .none, preferredStyle: .alert)
addItemBox.addTextField { (item) in
dataFromTextField = item
}
addButton = UIAlertAction(title: "Add", style: .cancel) { (action) in
if let data = dataFromTextField?.text, !data.isEmpty {
self.dummyData.append(Data(item: data, done: false))
} else {
self.dismiss(animated: true, completion: nil)
}
DispatchQueue.main.async {
self.todoTableView.reloadData()
}
}
addItemBox.addAction(addButton)
present(addItemBox, animated: true, completion: nil)
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate, SwipeTableViewCellDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dummyData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let todoCell = tableView.dequeueReusableCell(withIdentifier: "todoCell") as! TodoTableViewCell
todoCell.dataLabel.text = dummyData[indexPath.row].item
if (dummyData[indexPath.row].done == false) {
todoCell.dataLabel.hideStrikeTextLayer()
} else {
todoCell.dataLabel.strikeThroughText()
}
todoCell.delegate = self // Not sure it is needed
return todoCell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let index = IndexPath(row: indexPath.row, section: 0)
if dummyData[indexPath.row].done == false {
dummyData[indexPath.row].done = true
todoTableView.reloadRows(at: [index], with: .none)
} else {
dummyData[indexPath.row].done = false
todoTableView.reloadRows(at: [index], with: .none)
}
}

@Claude31 Finally I am able to solved this problem by attaching the DispatchQueue.main.async to the strikeLabel() and hideLabel() inside the cellForRowAtIndexPath() method. I can't really understand how things are going on here because using reload data are is working well but while using reloadRows() I have to wrap things to run on the main queue as stated above. Below is the code for the methods I have used, it is a third party library that I use to have a line crossing effect on label's text:

Code Block
//
// StrikethroughLabel.swift
// StrikethroughLabel
//
// Created by Charles Prado on 07/04/20.
// Copyright © 2020 Charles Prado. All rights reserved.
//
import UIKit
open class StrikethroughLabel: UILabel {
private var strikeTextLayers = [CAShapeLayer]()
public func hideStrikeTextLayer() {
self.strikeTextLayers.forEach { layer in layer.removeFromSuperlayer() }
}
public func showStrikeTextLayer() {
self.strikeThroughText(duration: 0.0)
}
public func strikeThroughText(duration: TimeInterval = 0.3, lineColor: UIColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)) {
self.strikeTextLayers.forEach { layer in layer.removeFromSuperlayer() }
func strikeThroughText(line: Int, inLines lines: [String]) {
let baseYPosition = (font.lineHeight * (CGFloat(line - 1) + 0.5))
guard baseYPosition < self.frame.height else { return }
let path = UIBezierPath()
path.move(to: CGPoint(x: -4, y: baseYPosition))
let attributedText = NSAttributedString(string: lines[line - 1].trimmingCharacters(in: .whitespaces),
attributes: [.font: self.font])
let lineMaxX = maxXForLine(withText: attributedText,
labelWidth: self.bounds.width) + 4
path.addLine(to: CGPoint(x: lineMaxX, y: baseYPosition))
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.lineWidth = 1
shapeLayer.path = path.cgPath
self.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = duration
shapeLayer.add(animation, forKey: "strikeThroughTextAnimation")
self.strikeTextLayers.append(shapeLayer)
}
let lines = self.lines(forLabel: self) ?? []
let numberOfLines = lines.count
for line in 1...numberOfLines {
strikeThroughText(line: line, inLines: lines)
}
}
private func lines(forLabel: UILabel) -> [String]? {
guard let text = text, let font = font else { return nil }
let attStr = NSMutableAttributedString(string: text)
attStr.addAttribute(NSAttributedString.Key.font, value: font, range: NSRange(location: 0, length: attStr.length))
let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path = CGMutablePath()
let size = sizeThatFits(CGSize(width: self.frame.width, height: .greatestFiniteMagnitude))
path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path, nil)
guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil }
var linesArray: [String] = []
for line in lines {
let lineRef = line as! CTLine
let lineRange = CTLineGetStringRange(lineRef)
let range = NSRange(location: lineRange.location, length: lineRange.length)
let lineString = (text as NSString).substring(with: range)
linesArray.append(lineString)
}
return linesArray
}
private func maxXForLine(withText text: NSAttributedString, labelWidth: CGFloat) -> CGFloat {
let labelSize = CGSize(width: labelWidth, height: .infinity)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: labelSize)
let textStorage = NSTextStorage(attributedString: text)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = .byWordWrapping
textContainer.maximumNumberOfLines = 0
let lastGlyphIndex = layoutManager.glyphIndexForCharacter(at: text.length - 1)
let lastLineFragmentRect = layoutManager.lineFragmentUsedRect(forGlyphAt: lastGlyphIndex,
effectiveRange: nil)
return lastLineFragmentRect.maxX
}
}



@OOPer Finally I am able to solved the issue, I have to wrap the third library methods inside cellForRowAtIndexPath() with DispatchQueue.main.async{} in order for it to work with tableView.reloadRows(). I cannot figure out what is happening here actually haha because with tableView.reloadData() its works well without any problems. Anyway, thank you so much for your suggestions and if you know what is the caused that lead me to wrap those methods with DispatchQueue.main.async{} as stated above please let me know hehe :). I also attached the source code of that third party library in my responses to @Claude31.
@imdavid1007@gmail.com

So, essentially, you did what I proposed by wrapping inside a
DispatchQueue.main.async { }

BTW, there is no DispatchQueue.main.async in the code you posted.

Anyway, you should now close the thread on the correct answer.

if you know what is the caused that lead me to wrap those methods with DispatchQueue.main.async{} as stated above please let me know hehe

Thanks for sharing your experience.
But unfortunately, whether enclosing with DispatchQueue.main.async{...} or not does not affect the behavior of my test project, so I have nothing to tell you right now.

Please share your info when you find something new, I will be watching on this thread.
tableview.reloadRows() takes three hit to reload
 
 
Q