Enable orphan words feature for NSMutableAttributedString - ios

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.

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.

It is a change by Apple since iOS11 (as answered by #DonMag) to prevent orphaned word in the last line.
If your production only support iOS13.0+, setting the lineBreakStrategy will set it back to the old style.
let label = UILabel()
if #available(iOS 14.0, *) {
label.lineBreakStrategy = NSParagraphStyle.LineBreakStrategy()
}
(One interesting thing is, I found this lineBreakStrategy also work on iOS 13.0+, even tho from Apple's document it mentioned iOS 14.0+.)
If you need to support older iOS version, you need to set the value of the NSAllowsDefaultLineBreakStrategy key when application launch, which I cannot find any document about it. I tested it worked on iOS 11 & 12, but not on iOS 13.0+.
// Setting the undocumented key NSAllowsDefaultLineBreakStrategy
UserDefaults.standard.set(false, forKey: "NSAllowsDefaultLineBreakStrategy")
So you might need both if your app support iOS 11.0+. Hope it helps ;)

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, and lineBreakStrategy properties. Set the NSForegroundColorAttributeName, NSFontAttributeName, alignment, lineBreakMode, and lineBreakStrategy 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.)

Related

Specifying NSLineBreakMode.byTruncatingTail causes issues when displaying NSAttributedString in UILabel

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:
Example 1 (everything ok)
Example 2 (everything ok)
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 1 (issues)
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>\n\n\n\n<p>Erneut trifft es uns und wir müssen die Sammlung vom Samstag <strong>bis auf Weiteres verschieben!</strong>
\n<p>\n\nLiebe Kollegen</p>\n\n\n\n<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>\n\n\n\n<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>Jahresprogramm_2019</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.
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:
myLabel.text = "Let's test this:\nPlenty of text to wrap onto more than three lines. Note this is plain text, not attributed text. It may or may not exhibit the same problems."
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
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:
Workaround
You may want to try this as a workaround...
class TextViewLabel: UITextView {
public var numberOfLines: Int = 0 {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
public var lineBreakMode: NSLineBreakMode = .byTruncatingTail {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() -> Void {
isScrollEnabled = false
isEditable = false
isSelectable = false
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}
}
Using TextViewLabel in place of UILabel will avoid the bug:
Edit 2
As of iOS 16.2 there are still inconsistencies with word-wrapping and tail-truncation between UILabel and UITextView.
Here is some code to see them side-by-side:
// a replacement for UILabel
class TextViewLabel: UITextView {
public var numberOfLines: Int = 0 {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
public var lineBreakMode: NSLineBreakMode = .byTruncatingTail {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() -> Void {
isScrollEnabled = false
isEditable = false
isSelectable = false
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}
}
class ViewController: UIViewController {
let labelStackView = UIStackView()
let textViewStackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
let scrollView = UIScrollView()
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.spacing = 8
hStack.distribution = .fillEqually
hStack.alignment = .top
[labelStackView, textViewStackView].forEach { v in
v.axis = .vertical
v.spacing = 4
hStack.addArrangedSubview(v)
}
hStack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(hStack)
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
hStack.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
hStack.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
hStack.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
hStack.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
hStack.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
])
let infoFont: UIFont = .italicSystemFont(ofSize: 10.0)
let testFont: UIFont = .systemFont(ofSize: 12.0)
var testStr: String = "This is some example text to test truncation consistency. At the end of this line, is a pair of newline chars:\n\nA HEADLINE\n\nThe headline was also followed by a pair of newline chars."
for i in 3...10 {
let infoLabel = UILabel()
infoLabel.font = infoFont
infoLabel.text = "UILabel - numLines: \(i)"
labelStackView.addArrangedSubview(infoLabel)
let v = UILabel()
v.font = testFont
v.numberOfLines = i
v.text = testStr
v.backgroundColor = .yellow
v.lineBreakMode = .byTruncatingTail
labelStackView.addArrangedSubview(v)
labelStackView.setCustomSpacing(16.0, after: v)
}
for i in 3...10 {
let infoLabel = UILabel()
infoLabel.font = infoFont
infoLabel.text = "TextViewLabel - numLines: \(i)"
textViewStackView.addArrangedSubview(infoLabel)
let v = TextViewLabel()
v.font = testFont
v.numberOfLines = i
v.text = testStr
v.backgroundColor = .cyan
v.lineBreakMode = .byTruncatingTail
textViewStackView.addArrangedSubview(v)
textViewStackView.setCustomSpacing(16.0, after: v)
}
scrollView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
}
}
Differences also exist from one iOS version to another - for example:
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 whole attributedText, as long as it's set after the attributedText 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's lineBreakMode.
NSMutableParagraphStyle has a property called lineBreakMode, 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!

How to add padding to a NSMutableAttributedString?

I have a label that uses a NSMutableAttributedString to write out the text as:
What I want to do is lower the asterisk's top padding so that it's midY is even with the word Cuisine like below:
How can I add padding using a NSMutableAttributedString?
I know I can create a separate label with the asterisk alone and use anchors w/ a constant to center it but I want to see how this is possible using a NSMutableAttributedString
let cuisineLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
let attributedText = NSMutableAttributedString(string: "Cuisine ", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17), NSAttributedStringKey.foregroundColor: UIColor.lightGray])
attributedText.append(NSAttributedString(string: "*", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 24), NSAttributedStringKey.foregroundColor: UIColor.red]))
label.attributedText = attributedText
return label
}()
The baselineOffset attribute key is used for this purpose.
let cuisine = NSMutableAttributedString(string: "Cuisine")
let asterisk = NSAttributedString(string: "*", attributes: [.baselineOffset: -3])
cuisine.append(asterisk)
Obviously, you will have to calculate the offset using the font size of the rest of the text. This is why I believe that using a full width asterisk (*) is easier.
Result with full width asterisk (you might want its font size to be a proportion of the font size of the rest of the string):
As Code Different points out, you can do this with baselineOffset attribute. A value of -8 should work for your case:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
let cuisineLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
let attributedText = NSMutableAttributedString(string: "Cuisine ", attributes: [
NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17),
NSAttributedStringKey.foregroundColor: UIColor.lightGray])
attributedText.append(NSAttributedString(string: "*", attributes: [
NSAttributedStringKey.font: UIFont.systemFont(ofSize: 24),
NSAttributedStringKey.baselineOffset: -8,
NSAttributedStringKey.foregroundColor: UIColor.red]))
label.attributedText = attributedText
return label
}()
view.addSubview(cuisineLabel)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
If you're struggling with line height offsets being messed up because of the new baseline and you're using a multi-line label, try playing with lineHeightMultiple:
let lineStyle = NSParagraphStyle()
lineStyle.lineHeightMultiple = 0.8
...
NSAttributedStringKey.paragraphStyle = style
If not (and you're using multiple labels stacked on top of one another) then you probably just need to adjust the frame of each label in the series to compensate.

UILabel text alignment not working (Swift 4)

I'm trying to align the text in a UILabel but it's not working. The text prints with the second line of the label hugging the left side of the label. How do I make both lines aligned in the center of the label? Thanks.
let bioTextView: UILabel = {
let tv = UILabel()
tv.text = "This is sample text for a bio label for a user on this platform."
tv.backgroundColor = .clear
tv.textColor = .white
tv.lineBreakMode = .byWordWrapping
tv.numberOfLines = 0
tv.textAlignment = NSTextAlignment.right
return tv
}()
Please find the below screenshot for the issue. My requirement is to fit the text in tow lines.
Try setting the alignment after you have added the view as a subview.
view.addSubview(self.bioTextView)
bioTextView.textAlignment = .center
Try this one:
let bioTextView: UILabel = {
let tv = UILabel()
tv.backgroundColor = .clear
tv.textColor = .white
tv.lineBreakMode = .byWordWrapping
tv.numberOfLines = 0
let textString = NSMutableAttributedString(
string: "This is sample text for a bio label for a user on this platform.",
attributes: [:])
let textRange = NSRange(location: 0, length: textString.length)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
textString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: textRange)
tv.attributedText = textString
return tv
}()
If someone is using UIStackView and has encountered this problem: stackView.alignment can ruin label.textAlignment property. Simply remove stackView.alignment.
Update following line fo code will fix your issue:
tv.textAlignment = .center
This will align your label text to center.

Xcode 8.3.3 - How can I make 2 rows in a label?

So basically, I have created a speedometer but right now I have it outputting "30km/h" on one line.
What I want it to look like is have 30 on one line and km/h below it.
And if anyone knows how to make that thicker line that goes up the faster you go, that would be a great help.
Here is what my code looks like right now:
let speed = (location.speed*3.6)
let speedInt: Int = Int(speed)
//statusLabel.backgroundColor = UIColor.red
//statusLabel.layer.cornerRadius = 10.0
//statusLabel.clipsToBounds = true
let statusLabel = UILabel()
let size:CGFloat = 70.0
statusLabel.textColor = UIColor.black
statusLabel.textAlignment = .center
statusLabel.font = UIFont.systemFont(ofSize: 13.0)
statusLabel.frame = CGRect(x : 172.0,y : 580.0,width : size, height : size)
statusLabel.layer.cornerRadius = size / 2
statusLabel.layer.borderWidth = 2.0
statusLabel.layer.backgroundColor = UIColor.white.cgColor
statusLabel.layer.borderColor = UIColor.init(colorLiteralRed: 14.0/255, green: 122.0/255, blue: 254.0/255, alpha: 1.0).cgColor
statusLabel.text = "\(speedInt) km/h"
You should use UILabel's attributedText property in order to achieve this look.
Use \n to insert a new line to the label.
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let bigAttr = [NSForegroundColorAttributeName: UIColor.white,
NSFontAttributeName: UIFont.systemFont(ofSize: 60, weight: UIFontWeightLight),
NSParagraphStyleAttributeName: paragraphStyle]
let smallAttr = [NSFontAttributeName: UIFont.systemFont(ofSize: 12, weight: UIFontWeightLight)]
let attributedString = NSMutableAttributedString(string: "\(speedInt)", attributes: bigAttr)
attributedString.append(NSAttributedString(string: "\nkm/h", attributes: smallAttr))
statusLabel.attributedText = attributedString
Add a \n where you want to split the string. "30 \n kmh"
Or simply add two labels.
You can simply add 2 lines in label with the help of UI. Just take a look at below snap
as shown above you can set the line break by word wrapping and simply add the title whole you want. in your case 0 km/h.
Then put the cursor at k and press ctrl + Enter. The title will appears in two lines.

Swift - UIButton with two lines of text

I was wondering if it is possible to create a UIButton with two lines of text. I need each line to have a different font size. The first line will be 17 point and the second will be 11 point. I've tried messing with putting two labels inside of a UIButton, but I can't get them to stay inside the bounds of the button.
I'm attempting to do all of this in the ui builder, and not programmatically.
Thanks
There are two questions.
I was wondering if it is possible to create a UIButton with two lines
of text
This is possible through using the storyboard or programmatically.
Storyboard:
Change the 'Line Break Mode' to Character Wrap or Word Wrap and use Alt/Option + Enter key to enter a new line in the UIButton's Title field.
Programmatically:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
btnTwoLine?.titleLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping;
}
I need each line to have a different font size
1
The worst case is, you can use a custom UIButton class and add two labels within it.
The better way is, make use of NSMutableAttributedString. Note that,this can be achieved through only programmatically.
Swift 5:
#IBOutlet weak var btnTwoLine: UIButton?
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
//applying the line break mode
textResponseButton?.titleLabel?.lineBreakMode = NSLineBreakMode.byWordWrapping;
let buttonText: NSString = "hello\nthere"
//getting the range to separate the button title strings
let newlineRange: NSRange = buttonText.range(of: "\n")
//getting both substrings
var substring1 = ""
var substring2 = ""
if(newlineRange.location != NSNotFound) {
substring1 = buttonText.substring(to: newlineRange.location)
substring2 = buttonText.substring(from: newlineRange.location)
}
//assigning diffrent fonts to both substrings
let font1: UIFont = UIFont(name: "Arial", size: 17.0)!
let attributes1 = [NSMutableAttributedString.Key.font: font1]
let attrString1 = NSMutableAttributedString(string: substring1, attributes: attributes1)
let font2: UIFont = UIFont(name: "Arial", size: 11.0)!
let attributes2 = [NSMutableAttributedString.Key.font: font2]
let attrString2 = NSMutableAttributedString(string: substring2, attributes: attributes2)
//appending both attributed strings
attrString1.append(attrString2)
//assigning the resultant attributed strings to the button
textResponseButton?.setAttributedTitle(attrString1, for: [])
}
Older Swift
#IBOutlet weak var btnTwoLine: UIButton?
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
//applying the line break mode
btnTwoLine?.titleLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping;
var buttonText: NSString = "hello\nthere"
//getting the range to separate the button title strings
var newlineRange: NSRange = buttonText.rangeOfString("\n")
//getting both substrings
var substring1: NSString = ""
var substring2: NSString = ""
if(newlineRange.location != NSNotFound) {
substring1 = buttonText.substringToIndex(newlineRange.location)
substring2 = buttonText.substringFromIndex(newlineRange.location)
}
//assigning diffrent fonts to both substrings
let font:UIFont? = UIFont(name: "Arial", size: 17.0)
let attrString = NSMutableAttributedString(
string: substring1 as String,
attributes: NSDictionary(
object: font!,
forKey: NSFontAttributeName) as [NSObject : AnyObject])
let font1:UIFont? = UIFont(name: "Arial", size: 11.0)
let attrString1 = NSMutableAttributedString(
string: substring2 as String,
attributes: NSDictionary(
object: font1!,
forKey: NSFontAttributeName) as [NSObject : AnyObject])
//appending both attributed strings
attrString.appendAttributedString(attrString1)
//assigning the resultant attributed strings to the button
btnTwoLine?.setAttributedTitle(attrString, forState: UIControlState.Normal)
}
Output
I was looking for nearly the same topic, except that I don't need two different font sizes. In case someone is looking for a simple solution:
let button = UIButton()
button.titleLabel?.numberOfLines = 0
button.titleLabel?.lineBreakMode = .byWordWrapping
button.setTitle("Foo\nBar", for: .normal)
button.titleLabel?.textAlignment = .center
button.sizeToFit()
button.addTarget(self, action: #selector(rightBarButtonTapped), for: .allEvents)
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: button)
I have notice an issue in most of the solutions which is while making line break mode to "Character Wrap" the second line will be left aligned to the first line
To make all the lines centered.
just change the title From Plain to Attributed and then you can make each line centered
change line break to character wrap , select your button and in attribute inspector go to line break and change it to character wrap
SWIFT 3 Syntax
let str = NSMutableAttributedString(string: "First line\nSecond Line")
str.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 17), range: NSMakeRange(0, 10))
str.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 12), range: NSMakeRange(11, 11))
button.setAttributedTitle(str, for: .normal)
I have fixed this and my solution it was only in the Storyboard.
Changes:
It added in Identity Inspector -> User Defined Runtime Attributes (these KeyPaths):
numberOfLines = 2
titleLabel.textAlignment = 1
User Defined Runtime Attributes
I added this in attributes inspector:
line break = word wrap
Word wrap
You need to do some of this in code. you can't set 2 different fonts in IB. In addition to changing the line break mode to character wrap, you need something like this to set the title,
override func viewDidLoad() {
super.viewDidLoad()
var str = NSMutableAttributedString(string: "First line\nSecond Line")
str.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(17), range: NSMakeRange(0, 10))
str.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(12), range: NSMakeRange(11, 11))
button.setAttributedTitle(str, forState: .Normal)
}
New with Xcode 13 (iOS 15)
Starting with Xcode 13, the button's title and subtitle may have their attributes set separately.
Using Storyboard:
In the Attribute Inspector for the button, select "Attributed" by Title. Then change font size of the title and the subtitle.
Or Programmatically:
// Create Title
let titleSettings = AttributeContainer.font( UIFont(name: "HelveticaNeue-Italic", size: 17)! )
yourButton.configuration?.attributedTitle = AttributedString("Button's Title", attributes: titleSettings)
// Create Subtitle
let subtitleSettings = AttributeContainer.font( UIFont(name: "HelveticaNeue-Italic", size: 11)! )
yourButton.configuration?.attributedSubtitle = AttributedString("Button's Subtitle", attributes: subtitleSettings)
One way to do it is with labels, I guess. I did this, and it seems to work ok. I could create this as a UIButton and then expose the labels, I guess. I don't know if this makes any sense.
let firstLabel = UILabel()
firstLabel.backgroundColor = UIColor.lightGrayColor()
firstLabel.text = "Hi"
firstLabel.textColor = UIColor.blueColor()
firstLabel.textAlignment = NSTextAlignment.Center
firstLabel.frame = CGRectMake(0, testButton.frame.height * 0.25, testButton.frame.width, testButton.frame.height * 0.2)
testButton.addSubview(firstLabel)
let secondLabel = UILabel()
secondLabel.backgroundColor = UIColor.lightGrayColor()
secondLabel.textColor = UIColor.blueColor()
secondLabel.font = UIFont(name: "Arial", size: 12)
secondLabel.text = "There"
secondLabel.textAlignment = NSTextAlignment.Center
secondLabel.frame = CGRectMake(0, testButton.frame.height * 0.5, testButton.frame.width, testButton.frame.height * 0.2)
testButton.addSubview(secondLabel)
The suggested solutions unfortunately did not work out for me when I wanted to have a mutliline button inside a CollectionView. Then a colleague showed me a workaround which I wanted to share in case someone has the same problem - hope this helps! Create a class which inherits from UIControl and extend it with a label, which will then behave similar like a button.
class MultilineButton: UIControl {
let label: UILabel = {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.numberOfLines = 0
$0.textAlignment = .center
return $0
}(UILabel())
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
])
}
override var isHighlighted: Bool {
didSet {
backgroundColor = backgroundColor?.withAlphaComponent(isHighlighted ? 0.7 : 1.0)
label.textColor = label.textColor.withAlphaComponent(isHighlighted ? 0.7 : 1.0)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
my way:
func setButtonTitle(title: String, subtitle: String, button: UIButton){
//applying the line break mode
button.titleLabel?.lineBreakMode = NSLineBreakMode.byWordWrapping;
let title = NSMutableAttributedString(string: title, attributes: Attributes.biggestLabel)
let subtitle = NSMutableAttributedString(string: subtitle, attributes: Attributes.label)
let char = NSMutableAttributedString(string: "\n", attributes: Attributes.biggestLabel)
title.append(char)
title.append(subtitle)
button.setAttributedTitle(title, for: .normal)
}

Resources