Can't reset UILabel attributedText when a UITableViewCell is reused
Asked Answered
S

5

20

The problem

I'm using a UITableView to show the list of transactions of a credit card. If the transaction is a chargeback, I'm adding a strikethrough style to the label:

Example of my UITableViewCells

The problem happens when that specific cell is reused. The strikethrought decoration is still there, even after resetting the text and attributedText property of the label.

Below I've added the relevant parts of my code:

Table view

class TimelineViewController: UIViewController {

    private lazy var tableView: UITableView = {
        let tableView = UITableView.init(frame: view.frame, style: .plain)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.register(TimelineTableViewCell.self, forCellReuseIdentifier: TimelineTableViewCell.reuseIdentifier)
        tableView.dataSource = self
        tableView.delegate = self
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        addTableViewOnView()
        getTimeline()
    }

}

extension TimelineViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: TimelineTableViewCell.reuseIdentifier,
                                                       for: indexPath) as? TimelineTableViewCell else { fatalError() }
        cell.transactionData = viewModel.timeline[indexPath.row]
        return cell
    }

}

Table view cell

class TimelineTableViewCell: UITableViewCell {

    static let reuseIdentifier = "TimelineTableViewCell"

    var transactionData: TimelineResponseModel! {
        didSet {
            // Reset the labels
            transactionDescriptionLabel.text = nil
            transactionDescriptionLabel.attributedText = nil
            transactionValueLabel.text = nil
            transactionValueLabel.attributedText = nil

            // Fill in the values
            transactionDescriptionLabel.text = transactionData.description
            transactionValueLabel.text = Formatter.currency.string(for: transactionData.balance)

            if transactionData.isChargeback {
                let value = Formatter.currency.string(for: transactionData.balance) ?? ""
                transactionDescriptionLabel.attributedText = transactionData.description.strikedThrough()
                transactionValueLabel.attributedText = value.strikedThrough()
            }
        }
    }

    private lazy var transactionDescriptionLabel: UILabel = {
        let transactionDescriptionLabel = UILabel()
        transactionDescriptionLabel.translatesAutoresizingMaskIntoConstraints = false
        transactionDescriptionLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
        transactionDescriptionLabel.adjustsFontForContentSizeCategory = true
        transactionDescriptionLabel.textColor = UIColor.activeText()
        transactionDescriptionLabel.numberOfLines = 0
        return transactionDescriptionLabel
    }()

    private lazy var transactionValueLabel: UILabel = {
        let transactionValueLabel = UILabel()
        transactionValueLabel.translatesAutoresizingMaskIntoConstraints = false
        transactionValueLabel.font = UIFont.preferredFont(forTextStyle: .caption1).bold()
        transactionValueLabel.adjustsFontForContentSizeCategory = true
        transactionValueLabel.textColor = UIColor.activeText()
        return transactionValueLabel
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        addTransactionDescriptionLabel()
        addTransactionValueLabel()
    }

}

Strikethrough style

extension String {

    func strikedThrough() -> NSAttributedString {
        let strikethroughStyle = [NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.single.rawValue]
        return NSAttributedString(string: self, attributes: strikethroughStyle)
    }

}

What I've tried

I've tried some approaches, without success:

  • Override prepareForReuse and reset the text and attributedText of the label there.
  • Create an else after the if transactionData.isChargeback to set the transactionDescriptionLabel and transactionValueLabel attributed text without the decoration

So, when the cell is reused, how can I reset it's labels to remove the strikethrough style I've added before?

Selfmoving answered 30/10, 2019 at 14:51 Comment(5)
Why are you setting the table cell content within TimelineTableViewCell as opposed to the delegate within TimelineViewController ? – Endothelium
@Endothelium To better encapsulate the UI elements inside the cell. Since I've declared them as private, the only attribute accessible from outside is the transactionData. When I set it, it updates the cell's UI elements. – Selfmoving
I suggest trying modifying the cell UI in the delegate just to see if it makes a difference. If doesn't work then there could be something wrong with your model not accounting for a new but already laid out cell. Good question :) – Endothelium
@Endothelium While trying your suggestion, I've stumbled upon the solution: for some weird reason, if I reset only the attributedText, the problem doesn't happen anymore. Or, if I reset the attributedText first, and then text later, the problem doesn't happen either. According to the documentation, assigning a new value to either property replaces the value of the other, so the order should not matter... Maybe that's a UILabel bug πŸ˜… – Selfmoving
I'm so glad you got to the solution with Alex! :) And yeah that is weird... I have come across something similar in the past which is why I mentioned the new but already laid out, I guess a similar thing occurred here with needing to reset the attributed text? Table cells are weird sometimes haha~ – Endothelium
B
2

You should try to set with .attributedText here instead of using `.text'. If it won't work I'll delete my answer.

// Fill in the values
transactionDescriptionLabel.text = transactionData.description
transactionValueLabel.text = Formatter.currency.string(for: transactionData.balance)

So, try this

transactionDescriptionLabel.attributedText = //
transactionValueLabel.attributedText = //

One more thing. Actually I don't like didSet. I suggest you to create a method to configure your cell. Here is an example of what I want to tell you.

func configure(with transactionData: TimelineResponseModel) {
   // Reset the labels
   transactionDescriptionLabel.text = nil
   transactionDescriptionLabel.attributedText = nil
   transactionValueLabel.text = nil
   transactionValueLabel.attributedText = nil

   // Fill in the values
   transactionDescriptionLabel.text = transactionData.description
   transactionValueLabel.text = Formatter.currency.string(for: transactionData.balance)

   if transactionData.isChargeback {
      let value = Formatter.currency.string(for: transactionData.balance) ?? ""
      transactionDescriptionLabel.attributedText = transactionData.description.strikedThrough()
      transactionValueLabel.attributedText = value.strikedThrough()
   }
}

Next.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: TimelineTableViewCell.reuseIdentifier,
                                                   for: indexPath) as? TimelineTableViewCell else { fatalError() }
    // Thats what I really like
    cell.configure(with: viewModel.timeline[indexPath.row])
    return cell
}
Baseman answered 30/10, 2019 at 17:23 Comment(4)
Yeah, that way it works! I haven't done this way at first because I don't need any different style if the transaction isn't a chargeback, but I guess I'll just create a NSAttributedString without any attributes for that. – Selfmoving
Glad to help you. So I want to share with you by my thoughts of using didSet. I will edit my answer. ) – Baseman
Thank you for the suggestion! Is there a particular reason why using didSet isn't ideal? I ask because I used to do exactly how you suggested, with a configure method, but decided to try the didSet property observers for this particular project. – Selfmoving
Actually nothing special with didSet, but didSet stores the value. Instead of this - configure method doesn't store anything, it's just like a { get set }. Hope I could help you :) – Baseman
S
20

Alex answer did fix my problem, but I've also found out why the reset of the labels didn't work. I'll leave it here in case it's useful for someone.

For some reason, if I reset only the attributedText, it works:

transactionDescriptionLabel.attributedText = nil
transactionValueLabel.attributedText = nil

Or, if I reset the attributedText first, and then reset the text, it also works:

transactionDescriptionLabel.attributedText = nil
transactionValueLabel.attributedText = nil
transactionDescriptionLabel.text = nil
transactionValueLabel.text = nil

According to the UILabel documentation, assigning a new value to either property replaces the value of the other, so the order should not matter. But it does, apparently.

https://developer.apple.com/documentation/uikit/uilabel/1620538-text

https://developer.apple.com/documentation/uikit/uilabel/1620542-attributedtext

Selfmoving answered 30/10, 2019 at 17:52 Comment(4)
This is super weird, but same for me! – Reeve
yes weird, happening to me still in 2020 :) only resetting attributedText before text solves the issue. – Zeba
Still the case in iOS 13.5. – Yolanthe
Thanks for this hint - you were right that you have to nil out both, but doing attributedText first actually solved the issue I was having. – Nauseating
E
3

Swift5 UILabel Extension

extension UILabel {
    func strikeThrough(_ isStrikeThrough: Bool = true) {
        guard let text = self.text else {
            return
        }
        
        if isStrikeThrough {
            let attributeString =  NSMutableAttributedString(string: text)
            attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: NSMakeRange(0,attributeString.length))
            self.attributedText = attributeString
        } else {
            let attributeString =  NSMutableAttributedString(string: text)
            attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: [], range: NSMakeRange(0,attributeString.length))
            self.attributedText = attributeString
        }
    }
}
Eachern answered 19/1, 2022 at 15:52 Comment(0)
T
3

I just found the same problem in our app on iOS 15.x. I have experimented with different ways to remove the attributed string before setting a new value, as the other answers advise.

Unfortunately, I haven't been able to get the reset to work. Then I realised that the problem happens only if the attribute (background color in my case) is covering the entire length of the text.

To prevent it from happening, I appended an invisible character to my attributed string and the problem got magically fixed.

let myString: NSMutableAttributedString = ...
myString.append(NSAttributedString(string: "\u{200B}")) // zero-width space

// set any attributes but do not set them on the last character
Typesetter answered 22/1, 2022 at 18:6 Comment(1)
I faced the same issue. My underline attribute didn't want to disappear for some cells in the table view. It really works! You saved my day, thank you! – Anitraaniweta
B
2

You should try to set with .attributedText here instead of using `.text'. If it won't work I'll delete my answer.

// Fill in the values
transactionDescriptionLabel.text = transactionData.description
transactionValueLabel.text = Formatter.currency.string(for: transactionData.balance)

So, try this

transactionDescriptionLabel.attributedText = //
transactionValueLabel.attributedText = //

One more thing. Actually I don't like didSet. I suggest you to create a method to configure your cell. Here is an example of what I want to tell you.

func configure(with transactionData: TimelineResponseModel) {
   // Reset the labels
   transactionDescriptionLabel.text = nil
   transactionDescriptionLabel.attributedText = nil
   transactionValueLabel.text = nil
   transactionValueLabel.attributedText = nil

   // Fill in the values
   transactionDescriptionLabel.text = transactionData.description
   transactionValueLabel.text = Formatter.currency.string(for: transactionData.balance)

   if transactionData.isChargeback {
      let value = Formatter.currency.string(for: transactionData.balance) ?? ""
      transactionDescriptionLabel.attributedText = transactionData.description.strikedThrough()
      transactionValueLabel.attributedText = value.strikedThrough()
   }
}

Next.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: TimelineTableViewCell.reuseIdentifier,
                                                   for: indexPath) as? TimelineTableViewCell else { fatalError() }
    // Thats what I really like
    cell.configure(with: viewModel.timeline[indexPath.row])
    return cell
}
Baseman answered 30/10, 2019 at 17:23 Comment(4)
Yeah, that way it works! I haven't done this way at first because I don't need any different style if the transaction isn't a chargeback, but I guess I'll just create a NSAttributedString without any attributes for that. – Selfmoving
Glad to help you. So I want to share with you by my thoughts of using didSet. I will edit my answer. ) – Baseman
Thank you for the suggestion! Is there a particular reason why using didSet isn't ideal? I ask because I used to do exactly how you suggested, with a configure method, but decided to try the didSet property observers for this particular project. – Selfmoving
Actually nothing special with didSet, but didSet stores the value. Instead of this - configure method doesn't store anything, it's just like a { get set }. Hope I could help you :) – Baseman
W
0

Li Jin Swift's Answer above correctly resets the StrikethroughStyle bit when assigned to a Label's AttributedText field.

Xamarin.iOS (C#) Example:

// does not reset StrikethroughStyle bit
NSMutableAttributedString attrString = new NSMutableAttributedString("Text", foregroundColor: UIColor.Black, strikethroughStyle: NSUnderlineStyle.None);  
                        
// does reset bit StrikethroughStyle when assigned to a label
attrString.AddAttribute(UIStringAttributeKey.StrikethroughStyle, NSNumber.FromLong(0), new NSRange(0, attrString.Length)); 
                            
Label.AttributedText = attrString;
Wolter answered 20/4, 2022 at 19:2 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.