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:
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?):
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:
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: