@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.
Post
Replies
Boosts
Views
Activity
@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:
//
//	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 I don't think those are the main cause because if I use todoTableView.reloadData() it works perfectly.
@Claude I have tried to not use the recompute indexPath before but it still performing the same.
@OOPer
//
// 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
}
}
@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.
@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?"
@Claude31 Thank you so much for your suggestions, I will wait for few more suggestions and I will give you a vote :)
@OOPer
I guess I am thinking wrong on using container viewcontroller, I think I suppose show a new viewcontroller on top of another viewcontroller instead of using a container viewcontroller.
@Claude31
Yes, line 24 got crash when I try to switch from my parent viewcontroller.
parent ViewController is SearchViewController while the child viewcontroller is SearchAccountViewController. I try to switch to the child ViewController whenever user tap on a UISearchBar.
I switch using:
class SearchViewController: UIViewController {
func showSearchAccountVC() {				
print("Begin to move into SearchAccountVC")				
let searchAccountVC = SearchAccountViewController()				
addChild(searchAccountVC)				
self.view.addSubview(searchAccountVC.view)				
searchAccountVC.didMove(toParent: self)			
searchAccountVC.view.frame = self.view.bounds
}
}
This is a delegate method from a UITableViewCell class which will trigger whenever user tap on a UISearchBar.
To sum up the problem, tableView is perfectly connected to the ViewController but whenever I try to switch to it from the parent ViewController, Xcode give me the "Unexpected Found nil" message saying that the tableviw is nil.
I recommend an iOS13 course by Dr. Angela Yu on Udemy!