NSAttributedString text always sticks to bottom with big lineHeight - ios

I'm trying to implement by-design labels coming from Sketch e.g. I need text styles with font size = 19 and line height = 50. So I ended up using NSAttributedString with NSMutableParagraphStyle but was stopped by problem with text being sticked to bottom of UILabel
I've already tried to use lineHeightMultiple and lineSpacing but those didn't give me the line height I wanted so I ended up using minimumLineHeight and maximumLineHeight equal the same
Here is my approach to make NSAttributedString
private static func makeAttributedString(
with attributes: TextAttributes,
text: String? = nil,
alignment: NSTextAlignment = .center
) -> NSAttributedString {
let font = UIFont(name: attributes.font.rawValue, size: attributes.fontSize)!
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = alignment
paragraph.paragraphSpacing = attributes.paragraph
paragraph.minimumLineHeight = attributes.lineHeight // equal 50 in my case
paragraph.maximumLineHeight = attributes.lineHeight // equal 50 in my case
let attributes: [NSAttributedStringKey: Any] = [
NSAttributedStringKey.paragraphStyle: paragraph,
NSAttributedStringKey.foregroundColor: attributes.textColor,
NSAttributedStringKey.kern: attributes.kern,
NSAttributedStringKey.font: font
]
return NSAttributedString(string: text ?? "", attributes: attributes)
}
I expect result similar to design
but actually getting
Note: setting height constraint to 50 is not applicable because I also need multiline labels but there is the same bug with them

Seems like I've found some workaround myself, maybe it will help someone.
The method is about setting baselineOffset like this:
NSAttributedStringKey.baselineOffset: (attributes.lineHeight - font.lineHeight) / 4
Works like charm:
https://i.imgur.com/a2EOf5R.png

Related

UIlabel NSMutableAttributedString second line not visible upon line break

I am trying to create a UILabel which contains two texts of different fonts where one NSMutableAttributedString sits vertically on top of the other. Upon attempting to insert a line break via swift's \n I found that the appended string disappears. I have tried a variety of lineBreakModes with no result (with and without \n) along with ensuring the frame isnt constricting the texts visibility by setting a large maximumLineHeight.
I should also mention that according to Apple's documentation when setting UILabel.attributedText to any NSAttributedText instance
When the label has an attributed string value, the system ignores the textColor, font, textAlignment, lineBreakMode, and lineBreakStrategy properties. Set the foregroundColor, font, alignment, lineBreakMode, and lineBreakStrategy properties in the attributed string instead.
Here is some of the code simplified for the sake of the question (I have also tried calling .sizeTofit() after setting the labels attributedText as well as setting different .lineBreakStrategys)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byWordWrapping
let totalVisitsString = NSMutableAttributedString(string: "\(visitLogs.count)\n", attributes: [.font : UIFont.systemFont(ofSize: 25), .paragraphStyle : paragraphStyle])
totalVisitsString.append(NSMutableAttributedString(string: "Total visits", attributes: [.font : UIFont.systemFont(ofSize: 14)]))
totalVisitsLabel.attributedText = totalVisitsString
the label itself:
var totalVisitsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
By default your label's numberOfLines is 1. You never change it so that is what you get.
Labels when created are defaulted to 1 line. You'll need to set the number of lines to 0 (unlimited) or whatever number you want to max it at.
var totalVisitsLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

NSAttributedString Drawing - How Many Lines?

I am drawing text within a PDF using this method:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: UIFont(name: "MySpecialFont", size: 16)!,
NSAttributedString.Key.foregroundColor: UIColor.black
]
let attributedText = NSAttributedString(
string: myTextArray.first,
attributes: textAttributes
)
let textRect = CGRect(
x: 400,
y: 40,
width: 80,
height: 40
)
attributedText.draw(in: textRect)
Above draws the text fine. However, sometimes, the string passed on to it seem to be too long and go on for 3 lines instead of 2. In these cases, I want to textRect to be taller. Basically to know how many lines it would take so textRect could be adjusted.
There are several functions within NSAttributedString that gives string length, but thats the length if it was in a single line.
Is there a way to know how many lines the final attributedText would take inside textRect?
Use NSAttributedString.boundingRect(with:options:context:) to compute the size required. You should pass .usesLineFragmentOrigin as an option so that it'll compute it for multiple lines. Pass the width you want; the height doesn't really matter, because it'll expand the height to contain the full string, and you can use that to work out your final rectangle.
That said, from your description, it sounds like you can just make the height very large (10,000 is the usual value; but maybe you want to just give room for three lines). Since you're just drawing here; it shouldn't matter if the available rect is taller than required. It only matters if it's shorter.

How to change line spacing for text in UILabel swift

I have looked at solutions on stack and they have not helped me. The following is my attempt to change the line spacing for the text in my UILabel. Please can someone advise. I see no effect on my line spacing.
let myText = abstractText.text!
let myParagraphStyle = NSMutableParagraphStyle()
myParagraphStyle.lineSpacing = 5
let myNsAttrStringObject = NSAttributedString.init(string: myText, attributes: ["paragraphStyle":myParagraphStyle])
abstractText.attributedText = myNsAttrStringObject
Your attribute key is not correct in this way, you can use objective-c key NSParagraphStyleAttributeName or swift 4 key NSAttributedStringKey.paragraphStyle:
let myNsAttrStringObject = NSAttributedString.init(string: myText, attributes: [NSParagraphStyleAttributeName: myParagraphStyle])

NSAttributedString with tabs

How do you create a UILabel with this kind of text format? Would you use NSAttributedString?
NSAttributedString can create text columns with tab stops. This is similar to how it is done in a word processor with the same limitations.
let text = "Name\t: Johny\nGender\t: Male\nAge\t: 25\nFavourites\t: Reading, writing"
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [NSTextTab(textAlignment: NSTextAlignment.Left, location: 150, options: [:])]
paragraphStyle.headIndent = 150
label.attributedText = NSAttributedString(string: text, attributes: [NSParagraphStyleAttributeName: paragraphStyle])
tabStops provides point positions for where to continue text after each tab. Here we did one tab at a reasonable point after the first column.
headIndent tells the label that wrapped text needs to be indented by a fixed amount, so it wraps to the next line.
The limitations with this approach are:
The tab stop location is a fixed point value so you need to know what you want. If the value you pick is less than the width of the first column for some lines, those lines will indent to a different location.
Wrapping only really works if your last column is the one that wraps. Since your second column was prefaced by ":" You may want to either just increase your headIndent or also split out the ":" to be \t:\t and set up a second tab stop. If you're not letting text wrap, this is not an issue.
If these limitations are too restrictive, you can restructure your label to be a collection of multiple labels with auto layout constraints.
In Swift 4.2 or above
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [NSTextTab.init(textAlignment: .left, location: 150, options: [:])]
paragraphStyle.headIndent = 150
let attributedTitle = NSAttributedString(string: "Some Title", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14.0), NSAttributedString.Key.paragraphStyle: paragraphStyle])

UITextView lineHeightMultiple Clips Top, first line, of Text

In iOS 8, I have a vanilla UITextView that clips the top of the 1st line when a lineHeightMultiple is applied to it's NSMutableParagraphStyle, see image below:
It appears as though lineHeightMultiple affects the 1st line of text in addition to subsequent lines.
Setting clipsToBounds = false on the UITextView will at least enable the clipped part to show, but you can see from the image below that now the top part of the text is obviously above it's frame:
I can fix this by just setting the top constraint on the offending UITextView to compensate for clipsToBounds = false but that feels like a hack to me.
I have also tried using a WKWebView for the offending text, and just setting the css line-height property, and that works just fine. There must simply be something I am missing when it comes to UITextView though.
Additionally, setting lineSpacing on the paragraph style to less than 0 has no affect, per the docs:
The distance in points between the bottom of one line fragment
and the top of the next.
**This value is always nonnegative.**
This value is included in the line fragment heights in the
layout manager.
I have also tried setting the contentInset of the UITextView as well as using a system font, both had not affect.
My sample code for this setup follows:
let text = "THIS IS A MULTILINE RUN OF TEXT"
let font = UIFont(name: "MyFontName", size: 31.0)!
// let font = UIFont.systemFontOfSize(31.0)
let paragraph = NSMutableParagraphStyle()
paragraph.lineHeightMultiple = 0.75
paragraph.alignment = NSTextAlignment.Center
let attributes = [
NSParagraphStyleAttributeName: paragraph,
NSFontAttributeName: font
]
titleView.attributedText = NSAttributedString(string: text, attributes: attributes)
// titleView.contentInset = UIEdgeInsets(top: 50.0, left: 0.0, bottom: 0.0, right: 0.0)
titleView.clipsToBounds = false
Has anyone encountered this and overcome or is the top constraint hack the only way to go?
I had the same issue.
I compensated it using NSBaselineOffsetAttributeName.
You should use:
let attributes = [
NSParagraphStyleAttributeName: paragraph,
NSFontAttributeName: font,
NSBaselineOffsetAttributeName: -5
]
You will have to also set a lower paragraph.lineHeightMultiple.
A little tricky, but it works.
Cooliopas is right, but I ended up using this in a label extension and needed something more suited for the many different sizes of text throughout my app. I found that the baseline adjustment to keep the text vertically centered was 10% of the height of the text.
let attrString = NSMutableAttributedString(string: newText)
let style = NSMutableParagraphStyle()
style.alignment = self.textAlignment
style.lineSpacing = 1.0
style.lineHeightMultiple = 0.75
var baselineOffset : CGFloat = -5 //-5 is a default for this in case there is no attributedText size to reference
if let size = self.attributedText?.size(){
baselineOffset = size.height * -0.1
} else {
NSLog("attributedText = nil, setting lineHeightMultiple to -5 as a default for ", newText)
}
attrString.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(0, attrString.length))
attrString.addAttribute(NSBaselineOffsetAttributeName, value: baselineOffset, range: NSMakeRange(0, attrString.length))
self.clipsToBounds = false
self.attributedText = attrString
Alternative approach - use a stack view with two labels, and set the stack view spacing to the top label's font's descender.

Resources