I have an Attributed string and I want to decrease the standard line height of it. To do so, I need to set a negative lineSpacing to my NSMutableParagraphStyle. But it's illegal according to Apple's docs.
Fun fact is that the negative lineSpacing actually works, but causes an extra bottom spacing in the UILabel which depends on the number of lines.
Is it possible to decrease the line height without having side effects?
Use NSParagraphStyle.lineHightMultiple
https://developer.apple.com/documentation/uikit/nsparagraphstyle/1528614-lineheightmultiple
You can set the lineHeightMultiple to a value greater than 0 but less than 1 and it'll reduce line spacing.
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 0.83 //Try different values here to see what looks best
let attrString = NSMutableAttributedString(string: "Your string")
attrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attrString.length))
You can also do this from storyboard:
Related
My designer send me sketch file which says 'Line height: 22' for label. How can i achieve this in xcode interface builder.
Is there any way to define this line height using code or UI builder.
#bbjay did put me on the right track.
If you want to obtain the exact result of Sketch, the formula is:
paragraphStyle .lineSpacing = sketchLineHeight - font.lineHeight
Provided that the font was given sketchFontSize
I've found the following formula to work well for me.
It converts form Sketch line height to iOS line spacing:
lineSpacing = sketchLineHeight - sketchFontSize - (font.lineHeight - font.pointSize)
In code, for your case this would be:
let font = UIFont.systemFont(ofSize: 18) // or whatever font you use
textLabel.font = font
let attributedString = NSMutableAttributedString(string: "your text")
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 22 - 18 - (font.lineHeight - font.pointSize)
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedString.length))
textLabel.attributedText = attributedString
Line height is coming from CSS, so your designer must have a web designer background. On the mobile platforms, we do not specify line height, but line spacing.
In general NSMutableParagraphStyle offers capabilities to modify multiline labels for iOS.
NSMutableParagraphStyle has a property called maximumLineHeight, but this will only set the maximum line height to a certain value, if the containment of the label would exceed a certain value.
To set this up in IB, you need to add the label, and change the Text property to Attributed. Than click on paragraph style icon, and set the line spacing for the label. Looking at the design, it is around 2 points of line spacing, what you need. You can either ask your designer to provide you with line spacing attribute or try to find the right line spacing value by randomly trying out different values.
In storyboard, use the Atributed style of UILabel. Below is example with 2.5 line height
When I draw an attributed string with a fixed line height with Text Kit, the characters always get aligned to the bottom of the line fragment. While this would make sense on one line with characters varying in size, this breaks the flow of the text with multiple lines. The baselines appear decided by the largest descender for each line.
I've found an article from the people behind Sketch explaining this exact problem in a bit more detail and showing what their solution does, but obviously not explaining how they achieved this.
This is what I want basically:
When showing two lines with a large line height, this result is far from ideal:
The code I'm using:
let smallFont = UIFont.systemFont(ofSize: 15)
let bigFont = UIFont.systemFont(ofSize: 25)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = 22
paragraphStyle.maximumLineHeight = 22
var attributes = [
NSFontAttributeName: smallFont,
NSParagraphStyleAttributeName: paragraphStyle
]
let textStorage = NSTextStorage()
let textContainer = NSTextContainer(size: CGSize(width: 250, height: 500))
let layoutManager = NSLayoutManager()
textStorage.append(NSAttributedString(string: "It is a long established fact that a reader will be ", attributes:attributes))
attributes[NSFontAttributeName] = bigFont
textStorage.append(NSAttributedString(string: "distracted", attributes:attributes))
attributes[NSFontAttributeName] = smallFont
textStorage.append(NSAttributedString(string: " by the readable content of a page when looking at its layout.", attributes:attributes))
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let textView = UITextView(frame: self.view.bounds, textContainer:textContainer)
view.addSubview(textView)
I managed to get this working, but had to drop support for iOS 8 and macOS 10.10 unfortunately.
If you implement the following delegate call of the NSLayoutManager, you get to decide what to do with the baselineOffset for each line fragment:
optional func layoutManager(_ layoutManager: NSLayoutManager,
shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer<CGRect>,
lineFragmentUsedRect: UnsafeMutablePointer<CGRect>,
baselineOffset: UnsafeMutablePointer<CGFloat>,
in textContainer: NSTextContainer,
forGlyphRange glyphRange: NSRange) -> Bool
When the NSTextStorage is created and for each subsequent change, I enumerate all used font, calculate it's default line height (NSLayoutManager.defaultLineHeightForFont()) and store the biggest line height. In the implementation of the above mentioned delegate method I check the current line height of the NSParagraphStyle for the provided line fragment and align the font's line height within that value. From there the baseline offset can be calculated with the knowledge that the baseline sits between the font's ascender and descender. Update the baselineOffset value with baselineOffset.memory(newOffset) and everything should be aligned as you'd like.
Note: I'm not going in too much detail about the actual code used to implement this because I'm not sure I'm using the right values throughout these calculations. I might update this in the near future when the whole approach is tried and proven.
Update: Implementation of adjusting baseline. Every time the textContainer changes I recalculate the biggest line height and biggest descender. Then I basically do this in the layout manager's delegate function:
var baseline: CGFloat = (lineFragmentRect.pointee.height - biggestLineHeight) / 2
baseline += biggestLineHeight
baseline -= biggestDescender
baseline = min(max(baseline, 0), lineFragmentRect.pointee.height)
baselineOffset.pointee = floor(baseline)
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])
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.
I have a UILabel that should display text on multiple lines in case that it's too long to stay on a single line. This how I set its parameters in interface builder:
But even by doing so, the text still gets truncated:
This is how I set the text at runtime:
let text = "left button pressed 5 seconds ago, you may want to press another button now"
let attributedText = NSMutableAttributedString(string: text)
attributedText.addAttribute(NSFontAttributeName, value: UIFont.boldSystemFontOfSize(statusLabel.font.pointSize), range: (text as NSString).rangeOfString("left"))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .ByWordWrapping
attributedText.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSMakeRange(0, attributedText.length))
statusLabel.attributedText = attributedText
Like you see I even tried to add a paragraph style attribute to force the text to stay on multiple lines, but it doesn't work.
Check that you're setting the auto layout constraints so you have the top, leading and trailing spaces defined, but don't hookup a vertical height, the label will adjust itself based on the content.
Edit: