The default behavior for UILabel is that it prevents orphan words to appear solely on a separate line. ie: if word wrapping happen to keep 1 word alone at the last line. iOS will prevent that by sending a word from the line before it, having two words in the last line.
The problem is that this feature doesn't work by default with NSMutableAttributedString
. how can I enable it?
Sample:
var string = customField?.title ?? ""
if customField?.required == true {
string = " *"
} else {
string = " (\(getLocalizedString(localizedKey: .optional)))"
}
let style = NSMutableParagraphStyle()
if #available(iOS 14.0, *) {
style.lineBreakStrategy = .standard
}
let att = NSMutableAttributedString(string: string, attributes: [.paragraphStyle: style])
titleLabel.attributedText = att
Have in mind I am forced to use NSMutableAttributedString
for other reasons. 2 labels won't work for me.
CodePudding user response:
As per OP's comments...
The issue is not with Attributed Text, as the same thing happens with "normal" text.
With iOS 11 (may have been 10), Apple changed UIKit to prevent orphans when a UILabel
wraps to two lines of text. Orphans are still allowed with more than two lines:
A
was prior to iOS 11... B
is current... C
is current with more than two lines...
Note the D
example -- I don't have the Xcode beta installed, but based on other comments I've seen it appears that in iOS 16 the "no orphan" rule will also be applied when the text wraps to more than two lines.
So... a way to solve your issue is to use a "non-break-space" character between the last word and the asterisk (instead of a plain space).
Here's a quick test:
class WrapTestVC: 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.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.widthAnchor.constraint(equalToConstant: 320.0),
])
var noteLabel: UILabel!
var testLabel: UILabel!
let noteFont: UIFont = .systemFont(ofSize: 14.0)
noteLabel = UILabel()
noteLabel.font = noteFont
noteLabel.numberOfLines = 0
noteLabel.text = "Just enough to fit:"
stackView.addArrangedSubview(noteLabel)
testLabel = UILabel()
testLabel.backgroundColor = .yellow
testLabel.numberOfLines = 0
testLabel.attributedText = sampleAttrString(method: 0)
stackView.addArrangedSubview(testLabel)
stackView.setCustomSpacing(20.0, after: testLabel)
noteLabel = UILabel()
noteLabel.font = noteFont
noteLabel.numberOfLines = 0
noteLabel.text = "Using a space char:"
stackView.addArrangedSubview(noteLabel)
testLabel = UILabel()
testLabel.backgroundColor = .yellow
testLabel.numberOfLines = 0
testLabel.attributedText = sampleAttrString(method: 1)
stackView.addArrangedSubview(testLabel)
stackView.setCustomSpacing(20.0, after: testLabel)
noteLabel = UILabel()
noteLabel.font = noteFont
noteLabel.numberOfLines = 0
noteLabel.text = "Using a non-break-space char:"
stackView.addArrangedSubview(noteLabel)
testLabel = UILabel()
testLabel.backgroundColor = .yellow
testLabel.numberOfLines = 0
testLabel.attributedText = sampleAttrString(method: 2)
stackView.addArrangedSubview(testLabel)
stackView.setCustomSpacing(20.0, after: testLabel)
noteLabel = UILabel()
noteLabel.font = noteFont
noteLabel.numberOfLines = 0
noteLabel.text = "Although, iOS 16 may give:"
stackView.addArrangedSubview(noteLabel)
testLabel = UILabel()
testLabel.backgroundColor = .yellow
testLabel.numberOfLines = 0
testLabel.attributedText = sampleAttrString(method: 3)
stackView.addArrangedSubview(testLabel)
stackView.setCustomSpacing(20.0, after: testLabel)
}
func sampleAttrString(method: Int) -> NSMutableAttributedString {
let fontA: UIFont = .systemFont(ofSize: 20.0, weight: .bold)
let attsA: [NSAttributedString.Key : Any] = [
.font: fontA,
.foregroundColor: UIColor.blue,
]
let attsB: [NSAttributedString.Key : Any] = [
.font: fontA,
.foregroundColor: UIColor.red,
]
var partOne = NSMutableAttributedString(string: "If the label has enough text so it wraps to more than two lines, UIKit will allow a last word orphan.", attributes: attsA)
var partTwo: NSAttributedString = NSAttributedString()
switch method {
case 0:
()
case 1:
partTwo = NSAttributedString(string: " *", attributes: attsB)
case 2:
partTwo = NSAttributedString(string: "\u{a0}*", attributes: attsB)
case 3:
partOne = NSMutableAttributedString(string: "If the label has enough text so it wraps to more than two lines, UIKit will allow a last\nword orphan.", attributes: attsA)
partTwo = NSAttributedString(string: "\u{a0}*", attributes: attsB)
default:
()
}
partOne.append(partTwo)
return partOne
}
}
Output:
So... you'll want to test that with iOS 16, and, if that's the case, you may need to do a version check to determine wether to add a plain space or a non-break-space.
CodePudding user response:
From the documentation of the lineBreakStrategy
property on UILabel
, which helps control this behavior:
When the label has an attributed string value, the system ignores the
textColor
,font
,textAlignment
,lineBreakMode
, andlineBreakStrategy
properties. Set theNSForegroundColorAttributeName
,NSFontAttributeName
,alignment
,lineBreakMode
, andlineBreakStrategy
properties in the attributed string instead.
If you want to use a specific line break strategy, like .standard
("The text system uses the same configuration of line-break strategies that it uses for standard UI labels. "), you will need to apply the attribute to the attributed string via a paragraph style:
let style = NSMutableParagraphStyle()
style.lineBreakStrategy = .standard
let text = NSMutableAttributedString(
string: "long title with an asterisk at the end *",
attributes: [.paragraphStyle: style]
)
titleLabel.attributedText = text
Depending on your text, it may also help to set allowsDefaultTighteningForTruncation
on the paragraph style because that may allow the text system to tighten the space between words on the last line of the string to get everything to fit. (I say may because this property controls truncation specifically, but it's possible that the text system can take it into account for wrapping as well.)