I faced with the following challenge. I want to make a very first line of text view like a headline. It should be multi line until user tap return button. This is the exact behaviour of Apple Notes view.
I already walked through lots of topics here, articles and libs and didn’t find anything suitable.
Could you please provide me with ideas how to implement similar behaviour or recommend me a library as an alternative.
Thanks in advance.
I've only done very quick testing with this, and it doesn't work exactly like Apple Notes, but it may be sufficient for your needs.
class FirstLineFormatVC: UIViewController, UITextViewDelegate {
let tv = UITextView()
let firstLineAttributes: [NSAttributedString.Key : Any] = [
.font: UIFont.systemFont(ofSize: 20.0, weight: .bold),
]
let normalAttributes: [NSAttributedString.Key : Any] = [
.font: UIFont.systemFont(ofSize: 16.0, weight: .regular),
]
override func viewDidLoad() {
super.viewDidLoad()
tv.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tv)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tv.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
tv.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
tv.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
tv.heightAnchor.constraint(equalToConstant: 300.0),
])
tv.delegate = self
// give it a background color so we can see it
tv.backgroundColor = .yellow
}
func textViewDidChange(_ textView: UITextView) {
//formatTextInTextView(textView: textView)
textView.isScrollEnabled = false
let selectedRange = textView.selectedRange
let text = textView.text ?? ""
// set full string to normalAttributes
let attributedString = NSMutableAttributedString(string: text, attributes: normalAttributes)
// get the first line
let range = text.lineRange(for: ..<text.startIndex)
let nsRange = NSRange(range, in: text)
// set first line to firstLineAttributes
attributedString.addAttributes(firstLineAttributes, range: nsRange)
// update the attributedText
textView.attributedText = attributedString
textView.selectedRange = selectedRange
textView.isScrollEnabled = true
}
}
Hopefully it's at least a starting point.
Related
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!
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.)
If you run the code snippet below, it will show incorrect text in the last line that has the ... ellipsis, as show in this image:
Once you change the lineBreakMode to byWordWrapping, or set the numberOfLines to 0, then the text is displayed correctly. Also, if you remove the line break /n from the text, then the text is displayed correctly.
So my question is: How can I use a /n line break in the text in a UILabel with a limited numberOfLines and lineBreakMode .byTruncatingTail without showing incorrect text on the last line?
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let text = "A line break challenge \nInvestigating a bug where the text in the last line is incorrect when numberOfLines is limited to for example 3. Very strange indeed. Peculiar. What to do about this? What am I missing?"
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byTruncatingTail //.byWordWrapping works, but not what I want.
let attributedString = NSAttributedString(string: text, attributes: [
.paragraphStyle: paragraphStyle
])
let label = UILabel()
label.attributedText = attributedString
label.numberOfLines = 3 //0 works, but is not what I want.
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 40.0).isActive = true
label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20.0).isActive = true
label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20.0).isActive = true
}
}
I'm trying to put the temperature in my app. I would like to show it like that:
But all I'm able to get is that:
I have tried to use this code to align the two label on top:
#IBDesignable class TopAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
if let stringText = text {
let stringTextAsNSString = stringText as NSString
let labelStringSize = stringTextAsNSString.boundingRect(with: CGSize(width: self.frame.width,height: CGFloat.greatestFiniteMagnitude), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil).size
super.drawText(in: CGRect(x:0,y: 0,width: self.frame.width, height:ceil(labelStringSize.height)))
} else {
super.drawText(in: rect)
}
}
}
It worked on for the '°C' but it's not working for the 25. Can someone help me find a solution to my problem ?
A very simple way to solve this is with attributed strings:
let tempText = "25˚C"
let baseFont = UIFont.systemFont(ofSize: 23.0)!
let superscriptFont = UIFont.systemFont(ofSize: 15.0)!
let attrStr = NSMutableAttributedString(string: tempText, attributes: [NSFontAttributeName: baseFont])
attrStr.addAttributes([NSFontAttributeName: superscriptFont, NSBaselineOffsetAttributeName: 10.0], range: NSMakeRange(2,2))
myLabel.attributedText = attrStr
You can keep adding more different attributes on any range you want by using the addAttributes method.
A font has multiple characteristics, including Ascender, Descender, CapHeight, etc... So what your code gets is not a way to align the character glyphs flush with the top of the label frame, but rather it aligns the Font bounding box to the top of the frame.
Calculating the offset between font metrics might give you what you're after. Here is one example (you can run it in a Playground page):
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let labelOne: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 60.0)
label.text = "25"
label.backgroundColor = .cyan
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 1
label.textAlignment = .right
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 24.0)
label.text = "°C"
label.backgroundColor = .cyan
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 1
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scroll view to self.view
self.view.addSubview(labelOne)
self.view.addSubview(labelTwo)
// constrain the scroll view to 8-pts on each side
labelOne.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0).isActive = true
labelOne.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
labelTwo.leftAnchor.constraint(equalTo: labelOne.rightAnchor, constant: 2.0).isActive = true
if let f1 = labelOne.font, let f2 = labelTwo.font {
let offset = (f1.ascender - f1.capHeight) - (f2.ascender - f2.capHeight)
labelTwo.topAnchor.constraint(equalTo: labelOne.topAnchor, constant: offset).isActive = true
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .yellow
PlaygroundPage.current.liveView = vc
And this is the result:
Depending on what font you are actually using, though, that may not be good enough. If so, you'll want to look into CoreText / CTFont / CTFontGetGlyphsForCharacters() / etc.
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)
}