Search code examples
iosswiftuikituilabel

UILabel Attributed Text Anomaly: Unexpected Strikethrough When Setting Text Property in Swift


The following code, will create a red color text, without strike-through.

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let text = "Hello World"
        let textCount = text.count
        let fullRange = NSRange(location: 0, length: textCount)
        
        var attributedText = NSMutableAttributedString(string: text)
        attributedText.addAttribute(.foregroundColor, value: UIColor.green, range: fullRange)
        attributedText.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: fullRange)
        label.attributedText = attributedText
        
        attributedText = NSMutableAttributedString(string: text)
        attributedText.addAttribute(.foregroundColor, value: UIColor.red, range: fullRange)
        attributedText.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange)
        label.attributedText = attributedText
    }
}

enter image description here


However, if I trigger label.text in between, it will cause the following strange behavior : A red color text, with strike-through created at the end of function.

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let text = "Hello World"
        let textCount = text.count
        let fullRange = NSRange(location: 0, length: textCount)
        
        
        var attributedText = NSMutableAttributedString(string: text)
        attributedText.addAttribute(.foregroundColor, value: UIColor.green, range: fullRange)
        attributedText.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: fullRange)
        label.attributedText = attributedText
        
        // Why this will cause a red color text, with strike-through created at the end of function?
        label.text = text
        
        attributedText = NSMutableAttributedString(string: text)
        attributedText.addAttribute(.foregroundColor, value: UIColor.red, range: fullRange)
        attributedText.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange)
        label.attributedText = attributedText
    }
}

enter image description here

Does anyone what is the reason behind this behavior, and how I can avoid such? Thank you.


Solution

  • Based on some searching and quick testing, this appears to be a long-standing issue. Bug? Quirk? Who knows...

    First note -- when your code does this:

        // 1
        attributedText = NSMutableAttributedString(string: text)
        // 2
        attributedText.addAttribute(.foregroundColor, value: UIColor.red, range: fullRange)
        // 3
        attributedText.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange)
        // 4
        label.attributedText = attributedText
    

    the // 3 line doesn't do anything ... you have not added the strikethrough attribute, so it's not there to remove.

    The only way I've seen to remove the strike through is to set it in your new attributed string with a value of 0 (zero):

        attributedText = NSMutableAttributedString(string: text)
        attributedText.addAttribute(.foregroundColor, value: UIColor.red, range: fullRange)
        // set strikeThrough to Zero
        attributedText.addAttribute(.strikethroughStyle, value: 0, range: fullRange)
        label.attributedText = attributedText
    

    There is a lot that goes on with .attributedText and .text ... if you want to understand why this is happening, let me know and I'll add some details.


    Edit - some additional info...

    Note: I don't work for Apple, and I don't have the source code for UILabel -- this is based solely on observation.

    Let's start by defining a couple elements:

    let textStr: String = "Hello World"
    
    var labelFont: UIFont = {
        guard let f = UIFont(name: "TimesNewRomanPS-ItalicMT", size: 40.0)
        else { fatalError("Could not create Label Font!!!") }
        return f
    }()
    
    var attribFont: UIFont = {
        guard let f = UIFont(name: "ChalkboardSE-Regular", size: 48.0)
        else { fatalError("Could not create Label Font!!!") }
        return f
    }()
    
    let labelColor: UIColor = .systemRed
    let attribColor: UIColor = .systemGreen
    
    // so we can see the label framing
    testLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
    

    We'll also set some "typical" properties of the label:

    // set label font and text color
    testLabel.font = labelFont
    testLabel.textColor = labelColor
    testLabel.textAlignment = .center
        
    

    If we set attributed text with an NSAttributedString with no additional attributes, the attributed text will "inherit" the label's properties:

    // attributed text with no attributes
    let attributedText = NSMutableAttributedString(string: textStr)
    testLabel.attributedText = attributedText
    

    output:

    enter image description here

    If we use a Font attribute, but not color:

    let textCount = textStr.count
    let fullRange = NSRange(location: 0, length: textCount)
    
    // attributed text with no attributes
    let attributedText = NSMutableAttributedString(string: textStr)
    
    // attributed text with ONLY font
    attributedText.addAttribute(.font, value: attribFont, range: fullRange)
    testLabel.attributedText = attributedText
    

    output:

    enter image description here

    As we see, the color is inherited.

    If we use font and color attributes:

    let textCount = textStr.count
    let fullRange = NSRange(location: 0, length: textCount)
    
    // attributed text with font and color
    attributedText.addAttribute(.foregroundColor, value: attribColor, range: fullRange)
    attributedText.addAttribute(.font, value: attribFont, range: fullRange)
    testLabel.attributedText = attributedText
    

    we get this (as expected):

    enter image description here

    Note that if we again set attributed text with no additional attributes, we get the label's properties again:

    let textCount = textStr.count
    let fullRange = NSRange(location: 0, length: textCount)
    
    // attributed text with font and color
    attributedText.addAttribute(.foregroundColor, value: attribColor, range: fullRange)
    attributedText.addAttribute(.font, value: attribFont, range: fullRange)
    testLabel.attributedText = attributedText
    
    let newStr = "Goodbye"
    let newAttributedText = NSMutableAttributedString(string: newStr)
    testLabel.attributedText = newAttributedText
    

    output:

    enter image description here

    Now, what if we set the .text property of a label that already has an .attributedText property... (and the attributes span the full length of the text)?

    In that case, the label inherits the attributes from the attributedText:

    let textCount = textStr.count
    let fullRange = NSRange(location: 0, length: textCount)
    
    let attributedText = NSMutableAttributedString(string: textStr)
    
    // attributed text with font and color
    attributedText.addAttribute(.foregroundColor, value: attribColor, range: fullRange)
    attributedText.addAttribute(.font, value: attribFont, range: fullRange)
    testLabel.attributedText = attributedText
    
    // set the .text property
    testLabel.text = textStr
    
    // the label has now inherited the font and color from the attributedText
    
    let newStr = "Goodbye"
    // no additional attributes
    let newAttributedText = NSMutableAttributedString(string: newStr)
    testLabel.attributedText = newAttributedText
    

    output:

    enter image description here

    So... what's happening with your .strikethroughStyle issue?

    let textCount = textStr.count
    let fullRange = NSRange(location: 0, length: textCount)
    
    let attributedText = NSMutableAttributedString(string: textStr)
    
    // attributed text with font, color AND strike-through
    attributedText.addAttribute(.foregroundColor, value: attribColor, range: fullRange)
    attributedText.addAttribute(.font, value: attribFont, range: fullRange)
    attributedText.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: fullRange)
    testLabel.attributedText = attributedText
        
    

    output:

    enter image description here

    A UIFont does not have a strike-through style -- at least, not exposed to us.

    So, if we set attributed text with a strike-through and then set the .text property:

    let textCount = textStr.count
    let fullRange = NSRange(location: 0, length: textCount)
    
    // attributed text with no attributes
    let attributedText = NSMutableAttributedString(string: textStr)
    
    // attributed text with font, color AND strike-through
    attributedText.addAttribute(.foregroundColor, value: attribColor, range: fullRange)
    attributedText.addAttribute(.font, value: attribFont, range: fullRange)
    attributedText.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: fullRange)
    testLabel.attributedText = attributedText
        
    // set the .text property
    //  the label inherits the attributes
    testLabel.text = ""
    

    the label has now inherited a Font with strike-through...

    and if we follow that with a Font only attributed string:

    let newStr = "Goodbye"
    let newFullRange = NSRange(location: 0, length: newStr.count)
    let newAttributedText = NSMutableAttributedString(string: newStr)
    // set ONLY the font
    let newFont: UIFont = .italicSystemFont(ofSize: 40.0)
    newAttributedText.addAttribute(.font, value: newFont, range: newFullRange)
    testLabel.attributedText = newAttributedText
    

    output:

    enter image description here

    we get the new font, and the label applies its properties/attributes - color and strike-through.

    To get rid of the strike-through, we need to set the attributed text with .strikethroughStyle set to Zero:

    newAttributedText.addAttribute(.strikethroughStyle, value: 0, range: newFullRange)
    

    output:

    enter image description here

    Keep in mind -- if we follow that with a new attributed string that does not address strike-through:

    let attributedTextB = NSMutableAttributedString(string: "What now?")
    testLabel.attributedText = attributedTextB
        
    

    the label still has the strike-through (and previously set font) attributes:

    enter image description here

    As a side-note ... if the attributes are not applied to the full string, the label will not inherit those attributes when setting the .text property.


    Bottom line:

    Avoid using the .text property when working with attributed strings.

    If you must do so, make sure you are "re-setting" attributes.


    Think about table view cells. A common beginner question related to cellForRowAt:

    if someCondition {
        cell.myLabel.textColor = .red
    }
    

    Why do the labels in all my cells turn red, even when someCondition is false?

    And the answer is -- cells are reused, and the color needs to be "reset":

    if someCondition {
        cell.myLabel.textColor = .red
    } else {
        cell.myLabel.textColor = .black
    }