Here's a simple test code to demonstrate the problem I am having. When the tableView first appears, all its rows have a strikethrough in the text of the cell's default textLabel
using attributedString. When I click on a row, it is supposed to remove the strikethrough attribute, but the attributed text remains unchanged. What am I doing wrong?
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableData = ["Green tomatoes", "Yellow bananas", "Red peppers"]
let cellId = "cellID"
override func viewDidLoad() {
super.viewDidLoad()
let tableview = UITableView(frame: CGRect(x: 0, y: 200, width: view.frame.width, height: view.frame.height))
tableview.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
tableview.rowHeight = 40
tableview.delegate = self
tableview.dataSource = self
view.addSubview(tableview)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 0.7176730633, green: 0.9274803996, blue: 0.9678700566, alpha: 1)
cell.textLabel?.attributedText = tableData[indexPath.row].addStrikeThrough()
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.textLabel?.attributedText = tableData[indexPath.row].removeStrikeThrough() // THIS DOESN'T WORK
tableView.reloadData() // This doesn't make any difference
}
}
extension String {
func addStrikeThrough() -> NSAttributedString {
let attributeString = NSMutableAttributedString(string: self)
attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle,
value: 2,
range: NSRange(location: 0, length: attributeString.length))
attributeString.addAttribute(NSAttributedString.Key.strikethroughColor,
value: UIColor.red,
range: NSMakeRange(0, attributeString.length))
return attributeString
}
func removeStrikeThrough() -> NSAttributedString {
let attributeString = NSMutableAttributedString(string: self)
attributeString.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: NSMakeRange(0, attributeString.length))
return attributeString
}
}
However, it works just fine for a regular UILabel:
class ViewController: UIViewController {
var label: UILabel!
let labelText = "Hello World"
override func viewDidLoad() {
label = UILabel()
label.attributedText = labelText.addStrikeThrough()
label.backgroundColor = .yellow
label.isUserInteractionEnabled = true
label.translatesAutoresizingMaskIntoConstraints = false
let tap = UITapGestureRecognizer(target: self, action: #selector(labelTapped))
label.addGestureRecognizer(tap)
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
label.heightAnchor.constraint(equalToConstant: 30)
])
}
@objc func labelTapped() {
label.attributedText = labelText.removeStrikeThrough() // THIS WORKS!
}
}
extension String {
func addStrikeThrough() -> NSAttributedString {
let attributeString = NSMutableAttributedString(string: self)
attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle,
value: 2,
range: NSRange(location: 0, length: attributeString.length))
attributeString.addAttribute(NSAttributedString.Key.strikethroughColor,
value: UIColor.red,
range: NSMakeRange(0, attributeString.length))
return attributeString
}
func removeStrikeThrough() -> NSAttributedString {
let attributeString = NSMutableAttributedString(string: self)
attributeString.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: NSMakeRange(0, attributeString.length))
return attributeString
}
}
CodePudding user response:
Okay, moving my comment into an answer, hopefully this will help you out.
Basically, a UITableView
uses a data source ad I'm not seeing code that updates it. The issue involves two things. First, this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.textLabel?.attributedText = tableData[indexPath.row].removeStrikeThrough() // THIS DOESN'T WORK
tableView.reloadData() // This doesn't make any difference
}
What I see is you are updating the cell, not the data source. Let me assume that your data source - tableData
has two things: some text, and a flag to indicate whether to use strikethrough (that for example purposes it set to true
. Simply update the data source by replacing this with:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableData[indexPath.row].useStrikethrough = false
tableView.reloadData() // This doesn't make any difference
}
Note, you are updating the data, not the cell!
Second, update things when you are displaying the cells like so:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 0.7176730633, green: 0.9274803996, blue: 0.9678700566, alpha: 1)
if tableData[indexPath.row].useStrikethrough {
cell.textLabel?.attributedText = tableData[indexPath.row].addStrikeThrough()
} else {
cell.textLabel?.attributedText = tableData[indexPath.row].addStrikeThrough().removeStrikethrough()
}
return cell
}
I'm using your code, so forgive the odd call to .addStrikeThrough().removeStrikethrough()
- which is only to show the main issue here. You can easily change that logic. The key things I hope I'm showing is that:
- Update
tableData
, not the cell itself, - Call
reloadData
- which you are doing correctly - next, - And Change the logic in
cellForRowAt
to tell the cell whether to use strikethrough.
CodePudding user response:
In your didSelectRowAt
you are dequeuing a cell. You should never call dequeueReusableCell()
outside of the cellForRowAt
datasource function.
You can use tableview.cellForRow(at:IndexPath)
to get the currently on-screen cell for the specified index path, if there is one.
So, you could say:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at:indexPath)
cell.textLabel?.attributedText = tableData[indexPath.row].removeStrikeThrough()
}
But don't do this
A table view cell should just reflect your data. They are reused as rows scroll in and out of view. If a row re-appears, cellForRowAt
is called again, and in your case the text will have the strike-through again.
If you change the cell directly and don't change the data then then next time that cell is called for you will see the old data.
You need a more sophisticated data model:
struct Item {
var name
var isStruck = true
mutating func unstrike() -> Void {
self.isStruck = false
}
var attributedText: NSAttributedText {
let baseText = NSMutableAttributedText(string: self.name)
if self.isStruck {
let range = NSRange(location:0, length: baseText.length)
baseText.addAttribute(NSAttributedString.Key.strikethroughStyle,
value: 2,
range: range)
baseText.addAttribute(NSAttributedString.Key.strikethroughColor,
value: UIColor.red,
range: range)
}
return baseText
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableData = [Item(name:"Green tomatoes"), Item(name:"Yellow bananas"), Item(name:"Red peppers")]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 0.7176730633, green: 0.9274803996, blue: 0.9678700566, alpha: 1)
cell.textLabel?.attributedText = tableData[indexPath.row].attributedText
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableData[indexPath.row].unstrike()
tableView.reloadRow(at: indexPath) // Don't reload the entire table unless all of the data has changed
}