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:
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:
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
}
}
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()
}
}
extension String {
func strikedThrough() -> NSAttributedString {
let strikethroughStyle = [NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.single.rawValue]
return NSAttributedString(string: self, attributes: strikethroughStyle)
}
}
I've tried some approaches, without success:
prepareForReuse
and reset the text
and attributedText
of the label there.else
after the if transactionData.isChargeback
to set the transactionDescriptionLabel
and transactionValueLabel
attributed text without the decorationSo, when the cell is reused, how can I reset it's labels to remove the strikethrough style I've added before?
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
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
}
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
}
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With