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) } } }
tableview.reloadRows() takes three hit to reload
Can you show enough code to reproduce the issue?
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.
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?"
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.
Sorry, but it was less than necessary.so I just paste the necessary part here.
Please show enough code to reproduce the issue. As I wrote I cannot reproduce the issue with your shown code.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.
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
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 } }
Thanks for showing your new code. But unfortunately, it does not reproduce the issue.@OOPer
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.
That cannot be a good reason.if I use todoTableView.reloadData() it works perfectly.
If you try not using SwipeCellKit and can reproduce the issue, it may not be the cause of the issue.
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) } }
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 } }
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.
Thanks for sharing your experience.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
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.