Home > Blockchain >  Specifying NSLineBreakMode.byTruncatingTail causes issues when displaying NSAttributedString in UILa
Specifying NSLineBreakMode.byTruncatingTail causes issues when displaying NSAttributedString in UILa

Time:08-20

In my app, I fetch some HTML strings from the Wordpress REST API of a website. I use the following extension to convert the HTML to an NSAttributedString that I can display in my UILabel:

extension NSAttributedString {
    
    convenience init(htmlString html: String, font: UIFont? = nil, useDocumentFontSize: Bool = false) throws {
        let options: [NSAttributedString.DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]

        let data = html.data(using: .utf8, allowLossyConversion: true)
        guard (data != nil), let fontFamily = font?.familyName, let attr = try? NSMutableAttributedString(data: data!, options: options, documentAttributes: nil) else {
            try self.init(data: data ?? Data(html.utf8), options: options, documentAttributes: nil)
            return
        }

        let fontSize: CGFloat? = useDocumentFontSize ? nil : font!.pointSize
        let range = NSRange(location: 0, length: attr.length)
        attr.enumerateAttribute(.font, in: range, options: .longestEffectiveRangeNotRequired) { attrib, range, _ in
            if let htmlFont = attrib as? UIFont {
                let traits = htmlFont.fontDescriptor.symbolicTraits
                var descrip = htmlFont.fontDescriptor.withFamily(fontFamily)

                if (traits.rawValue & UIFontDescriptor.SymbolicTraits.traitBold.rawValue) != 0 {
                    descrip = descrip.withSymbolicTraits(.traitBold)!
                }

                if (traits.rawValue & UIFontDescriptor.SymbolicTraits.traitItalic.rawValue) != 0 {
                    descrip = descrip.withSymbolicTraits(.traitItalic)!
                }

                attr.addAttribute(.font, value: UIFont(descriptor: descrip, size: fontSize ?? htmlFont.pointSize), range: range)
                attr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.label, range: range)
            }
        }

        self.init(attributedString: attr)
    }

}

The UILabel is defined as follows:

let myLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.systemFont(ofSize: 15, weight: .regular)
        return label
}()

I use the following code to display the NSAttributedString in the UILabel:

let contentAttributed = try? NSAttributedString(htmlString: html, font: UIFont.systemFont(ofSize: 15, weight: .regular))
myLabel.attributedText = contentAttributed
myLabel.numberOfLines = 3

With this, everything works as expected, as shown in two examples below:

enter image description here

Which doesn't make much sense... Apparently, a newline character gets factored in at the end of the attributed string -- but even so, we get almost, but not enough space for TWO additional lines (I'm guessing it's a newline and a paragraph vertical space?):

enter image description here


Second note:

If we DO NOT have enough text to exceed the bounds of the label (again, we're setting myLabel.numberOfLines = 3 here), we get another oddity. This is when I rotate the device:

enter image description here

Even though the text is NOT truncated, we get the ... -- because UIKit is trying to add 1.5 additional blank lines at the end of the string.


Third note:

As I mentioned in a comment, I know we came across this once before here on SO -- unfortunately, I can't remember if we resolved it or (probably) not.


To reproduce with Plain Text

class WrapBugVC: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 4
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            stackView.widthAnchor.constraint(equalToConstant: 315.0),
            stackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
        ])

        var vInfo: UILabel
        
        let testStr1: String = "Let's test this (no newline character): Plenty of text to wrap onto more than three lines. Note this is plain text, not attributed text."
        let testStr2: String = "Let's test this:\nPlenty of text to wrap onto more than three lines. Note this is plain text, not attributed text."

        vInfo = UILabel()
        vInfo.font = .italicSystemFont(ofSize: 15.0)
        vInfo.text = "NO newline, .byWordWrapping"
        stackView.addArrangedSubview(vInfo)
        
        let v1 = UILabel()
        v1.numberOfLines = 3
        v1.text = testStr1
        v1.backgroundColor = .yellow
        v1.lineBreakMode = .byWordWrapping
        stackView.addArrangedSubview(v1)
        
        vInfo = UILabel()
        vInfo.font = .italicSystemFont(ofSize: 15.0)
        vInfo.text = "NO newline, .byTruncatingTail"
        stackView.addArrangedSubview(vInfo)
        
        let v2 = UILabel()
        v2.numberOfLines = 3
        v2.text = testStr1
        v2.backgroundColor = .cyan
        v2.lineBreakMode = .byTruncatingTail
        stackView.addArrangedSubview(v2)
        
        vInfo = UILabel()
        vInfo.font = .italicSystemFont(ofSize: 15.0)
        vInfo.text = "WITH newline, .byWordWrapping"
        stackView.addArrangedSubview(vInfo)
        
        let v3 = UILabel()
        v3.numberOfLines = 3
        v3.text = testStr2
        v3.backgroundColor = .yellow
        v3.lineBreakMode = .byWordWrapping
        stackView.addArrangedSubview(v3)

        vInfo = UILabel()
        vInfo.font = .italicSystemFont(ofSize: 15.0)
        vInfo.text = "WITH newline, .byTruncatingTail"
        stackView.addArrangedSubview(vInfo)
        
        let v4 = UILabel()
        v4.numberOfLines = 3
        v4.text = testStr2
        v4.backgroundColor = .cyan
        v4.lineBreakMode = .byTruncatingTail
        stackView.addArrangedSubview(v4)

        [v1, v2, v3, v4].forEach { v in
            stackView.setCustomSpacing(16.0, after: v)
        }
        
    }
    
}

Result:

enter image description here

  • Related