Home > Software design >  Cannot remove strikethrough attribute in UILabel inside a UITableViewCell
Cannot remove strikethrough attribute in UILabel inside a UITableViewCell

Time:09-21

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
    }
  • Related