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:
I would really like the UILabels to be completely filled and with ellipsis at the end (truncated lines). If I add myLabel.lineBreakMode = .byTruncatingTail
into my code, there are weird issues, as shown in the same examples below. The weird thing is that some of the UILabels still work absolutely fine (example 2), while others show the weird issues (example 1).
Example 2 (still everything ok)
If anyone knows where in this rather complicated issue I should start digging, any help would be greatly appreciated.
Edit: Some examples of HTML strings that produce the issue:
n<p>Liebe Damen & Herren</p>nnnn<p>Erneut trifft es uns und wir müssen die Sammlung vom Samstag <strong>bis auf Weiteres verschieben!</strong>
n<p>nnLiebe Kollegen</p>nnnn<p>Am 28. August führen wir (endlich) wieder die Papiersammlung in der Gemeinde durch. Dabei sind wir auf möglichst viele Helfer:innen angewiesen.</p>
n<p>Liebe Kollegen</p>nnnn<p>Nur ungerne stören wir eure Sommerferien mit einer unerfreulichen Nachricht: <strong>Das Katre 2021 vom 4. und 5. September wurde leider abgesagt!</strong></p>
The following HTML strings do not produce the issue:
n<p>Kürzlich hat der Bundesrat über die in Etappen stattfindende Lockerung der Massnahmen zum Schutz vor dem Coronavirus informiert. Leider haben die ersten Lockerungen des Bundes noch keinen Einfluss auf uns.</p>
<p>Liebe Eltern und Kollegen</p>n<p>Unser Jahresprogramm für 2019 ist nun fertig und online. Ihr findet es unter Medien -> Downloads oder direkt hier:</p>n<p><a href="https: //somelink.com">Jahresprogramm_2019</a></p>
The App is run on the iPhone 13 Pro Simulator in portrait orientation. The UILabel is part of a custom UITableViewCell. The UITableView is aligned to the safe area on the left and right (constant 0). The UILabel is aligned to the contentView.leadingAnchor and contentView.trailingAnchor with a constant of 30 and -30, respectively.
2
Answers
It looks like the strings that work only have one paragraph of text, whereas the ones that do not have multiple. Apple’s documentation implies that
lineBreakMode
applies to the label’s wholeattributedText
, as long as it’s set after theattributedText
is assigned a value. That doesn’t explain this behavior though, and the fact that parts of the string are being repeated makes me think this is a bug on how multi-paragraph NSAttributedStrings interact with a label’slineBreakMode
.NSMutableParagraphStyle
has a property calledlineBreakMode
, which will allow you to set the line break mode for each paragraph individually. That might give you a few different options for workarounds.Alternatively – setting the
numberOfLines
to 0 also fixes the problem for me, though it’s not ideal since it might require you to set a max height for the label, which could worsen the user experience with dynamic font sizes.Viel Glück!
I’m posting this answer, not because it is a solution but because I think it is worth discussion…
Edit
Personally, I would say Attributed Text and.lineBreakMode = .byTruncatingTail
is rather buggy.This does not appear to be related to Attributed Text, but rather to the presence of a newline character.
For example, if the label is set to
.numberOfLines = 3
and we use this text:we will see the same corrupted text wrapping occur.
Also note: this does NOT happen on iOS prior to
15.4
!/Edit
In all the following examples, the same attributed string is used for pairs of rows, but only the cyan labels have
myLabel.lineBreakMode = .byTruncatingTail
.First note:
If we set
myLabel.numberOfLines = 0
, we get this output: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
Result:
Workaround
You may want to try this as a workaround…
Using
TextViewLabel
in place ofUILabel
will avoid the bug:Edit 2
As of
iOS 16.2
there are still inconsistencies with word-wrapping and tail-truncation betweenUILabel
andUITextView
.Here is some code to see them side-by-side:
Differences also exist from one iOS version to another – for example: