Core Text - NSAttributedString line height done right? - nsattributedstring

I'm completely in the dark with Core Text's line spacing. I'm using NSAttributedString and I specify the following attributes on it:
- kCTFontAttributeName
- kCTParagraphStyleAttributeName
From this the CTFrameSetter gets created and drawn to context.
In the paragraph style attribute I'd like to specify the height of the lines.
When I use kCTParagraphStyleSpecifierLineHeightMultiple each line receives padding at the top of the text, instead of the text being displayed in the middle of this height.
When I use kCTParagraphStyleSpecifierLineSpacing a padding is added to the bottom of the text.
Please help me achieve a specified line height with the text(glyphs) in the middle of that height, instead of the text sitting either at the bottom or the top of the line.
Is this not possible without going down the route of explicitly creating CTLine 's and so forth?

Objective-C
NSInteger strLength = [myString length];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
[style setLineSpacing:24];
[attString addAttribute:NSParagraphStyleAttributeName
value:style
range:NSMakeRange(0, strLength)];
Swift 5
let strLength = myString.length()
var style = NSMutableParagraphStyle()
style.lineSpacing = 24
attString.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: strLength))

I'm still not 100% confident in my following statements, but it seems to make sense. Please correct me where I am wrong.
The line height (leading) refers to the distance between the baselines of successive lines of type. The baseline here can be interpreted as the imaginary line which the text sits on.
Spacing is the space between lines. The space appears after the line of text.
I ended up using the following solution to my problem:
// NOT SURE WHAT THE THEORY BEHIND THIS FACTOR IS. WAS FOUND VIA TRIAL AND ERROR.
CGFloat factor = 14.5/30.5;
CGFloat floatValues[4];
floatValues[0] = self.lineHeight * factor/(factor + 1);
floatValues[1] = self.lineHeight/(factor + 1);
floatValues[2] = self.lineHeight;
This matrix is used with the paragraph style parameter for NSAttributedString:
CTParagraphStyleSetting paragraphStyle[3];
paragraphStyle[0].spec = kCTParagraphStyleSpecifierLineSpacing;
paragraphStyle[0].valueSize = sizeof(CGFloat);
paragraphStyle[0].value = &floatValues[0];
paragraphStyle[1].spec = kCTParagraphStyleSpecifierMinimumLineHeight;
paragraphStyle[1].valueSize = sizeof(CGFloat);
paragraphStyle[1].value = &floatValues[1];
paragraphStyle[2].spec = kCTParagraphStyleSpecifierMaximumLineHeight;
paragraphStyle[2].valueSize = sizeof(CGFloat);
paragraphStyle[2].value = &floatValues[2];
CTParagraphStyleRef style = CTParagraphStyleCreate((const CTParagraphStyleSetting*) &paragraphStyle, 3);
[attributedString addAttribute:(NSString*)kCTParagraphStyleAttributeName value:(id)style range:NSMakeRange(0, [string length])];
CFRelease(style);
Hope this helps someone. I'll update this answer as I discover more relevant information.

In Swift 3:
let textFont = UIFont(name: "Helvetica Bold", size: 20)!
let textColor = UIColor(white: 1, alpha: 1) // White
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.paragraphSpacing = 20 // Paragraph Spacing
paragraphStyle.lineSpacing = 40 // Line Spacing
let textFontAttributes = [
NSFontAttributeName: textFont,
NSForegroundColorAttributeName: textColor,
NSParagraphStyleAttributeName: paragraphStyle
] as [String : Any]

You can set/update line spacing and line height multiple from storyboard as well as programatically.
From Interface Builder:
Programmatically:
SWift 4
extension UILabel {
// Pass value for any one of both parameters and see result
func setLineSpacing(lineSpacing: CGFloat = 0.0, lineHeightMultiple: CGFloat = 0.0) {
guard let labelText = self.text else { return }
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
paragraphStyle.lineHeightMultiple = lineHeightMultiple
let attributedString:NSMutableAttributedString
if let labelattributedText = self.attributedText {
attributedString = NSMutableAttributedString(attributedString: labelattributedText)
} else {
attributedString = NSMutableAttributedString(string: labelText)
}
// Line spacing attribute
// Swift 4.2++
attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
// Swift 4.1--
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
self.attributedText = attributedString
}
}
Now call extension function
let label = UILabel()
let stringValue = "How to\ncontrol\nthe\nline spacing\nin UILabel"
// Pass value for any one argument - lineSpacing or lineHeightMultiple
label.setLineSpacing(lineSpacing: 2.0) . // try values 1.0 to 5.0
// or try lineHeightMultiple
//label.setLineSpacing(lineHeightMultiple = 2.0) // try values 0.5 to 2.0
Or using label instance (Just copy & execute this code to see result)
let label = UILabel()
let stringValue = "How to\ncontrol\nthe\nline spacing\nin UILabel"
let attrString = NSMutableAttributedString(string: stringValue)
var style = NSMutableParagraphStyle()
style.lineSpacing = 24 // change line spacing between paragraph like 36 or 48
style.minimumLineHeight = 20 // change line spacing between each line like 30 or 40
// Swift 4.2++
// Line spacing attribute
attrString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: stringValue.characters.count))
// Character spacing attribute
attrString.addAttribute(NSAttributedString.Key.kern, value: 2, range: NSMakeRange(0, attrString.length))
// Swift 4.1--
// Line spacing attribute
attrString.addAttribute(NSAttributedStringKey.paragraphStyle, value: style, range: NSRange(location: 0, length: stringValue.characters.count))
// Character spacing attribute
attrString.addAttribute(NSAttributedStringKey.kern, value: 2, range: NSMakeRange(0, attrString.length))
label.attributedText = attrString
Swift 3
let label = UILabel()
let stringValue = "How to\ncontrol\nthe\nline spacing\nin UILabel"
let attrString = NSMutableAttributedString(string: stringValue)
var style = NSMutableParagraphStyle()
style.lineSpacing = 24 // change line spacing between paragraph like 36 or 48
style.minimumLineHeight = 20 // change line spacing between each line like 30 or 40
attrString.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSRange(location: 0, length: stringValue.characters.count))
label.attributedText = attrString

I tried all these answers, but to really get the EXACT line height that usually comes in design files from Sketch or Zeplin then you need to:
let ps = NSMutableParagraphStyle()
ps.minimumLineHeight = 34
ps.maximumLineHeight = 34
let attrText = NSAttributedString(
string: "Your long multiline text that will have exact line height spacing",
attributes: [
.paragraphStyle: ps
]
)
someLabel.attributedText = attrText
someLabel.numberOfLines = 2
...

I made an extension for this, see below. With the extension you can just set the line height like so:
let label = UILabel()
label.lineHeight = 19
This is the extension:
// Put this in a file called UILabel+Lineheight.swift, or whatever else you want to call it
import UIKit
extension UILabel {
var lineHeight: CGFloat {
set {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = newValue
paragraphStyle.maximumLineHeight = newValue
_setAttribute(key: NSAttributedString.Key.paragraphStyle, value: paragraphStyle)
}
get {
let paragraphStyle = _getAttribute(key: NSAttributedString.Key.paragraphStyle) as? NSParagraphStyle
return paragraphStyle?.minimumLineHeight ?? 0
}
}
func _getAttribute(key: NSAttributedString.Key) -> Any? {
return attributedText?.attribute(key, at: 0, effectiveRange: .none)
}
func _setAttribute(key: NSAttributedString.Key, value: Any) {
let attributedString: NSMutableAttributedString!
if let currentAttrString = attributedText {
attributedString = NSMutableAttributedString(attributedString: currentAttrString)
} else {
attributedString = NSMutableAttributedString(string: text ?? "")
text = nil
}
attributedString.addAttribute(key,
value: value,
range: NSRange(location: 0, length: attributedString.length))
attributedText = attributedString
}
}
Notes:
I don't like line height multiples. My design document contains a height, like 20, not a multiple.
lineSpacing as in some other answers is something totally different. Not what you want.
The reason there's an extra _set/_getAttribute method in there is that I use the same method for setting letter spacing. Could also be used for any other NSAttributedString values but seems like I'm good with just letter spacing (kerning in Swift/UIKit) and line height.

There are two properties of NSParagraphStyle that modify the height between successive text baselines in the same paragraph: lineSpacing and lineHeightMultiple. #Schoob is right that a lineHeightMultiple above 1.0 adds additional space above the text, while a lineSpacing above 0.0 adds space below the text. This diagram shows how the various dimensions are related.
To get the text to stay centred the aim is therefore to specify one in terms of the other, in such a way that any 'padding' we add by one attribute (top/bottom) is balanced by determining the other attribute's padding (bottom/top) to match. In other words, any extra space added is distributed evenly while otherwise preserving the text's existing positioning.
The nice thing is that this way you can choose which attribute you want to specify and then just determine the other:
extension UIFont
{
func lineSpacingToMatch(lineHeightMultiple: CGFloat) -> CGFloat {
return self.lineHeight * (lineHeightMultiple - 1)
}
func lineHeightMultipleToMatch(lineSpacing: CGFloat) -> CGFloat {
return 1 + lineSpacing / self.lineHeight
}
}
From here, other answers show how these two attributes can be set in an NSAttributedString, but this should answer how the two can be related to 'centre' the text.

Swift 4 & 5
extension NSAttributedString {
/// Returns a new instance of NSAttributedString with same contents and attributes with line spacing added.
/// - Parameter spacing: value for spacing you want to assign to the text.
/// - Returns: a new instance of NSAttributedString with given line spacing.
func withLineSpacing(_ spacing: CGFloat) -> NSAttributedString {
let attributedString = NSMutableAttributedString(attributedString: self)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.lineSpacing = spacing
attributedString.addAttribute(.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: string.count))
return NSAttributedString(attributedString: attributedString)
}
}

This worked for me in Xcode 7.2. iOS 9.2.1. (Swift 2.1.):
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let paragraphStyleWithSpacing = NSMutableParagraphStyle()
paragraphStyleWithSpacing.lineSpacing = 2.0 //CGFloat
let textWithLineSpacing = NSAttributedString(string: str, attributes: [NSParagraphStyleAttributeName : paragraphStyleWithSpacing])
self.MY_TEXT_VIEW_NAME.attributedText = textWithLineSpacing
}

Another way of twerking with a NSAttributedString line position is playing with
baselineOffset attribute:
let contentText = NSMutableAttributedString(
string: "I see\nI'd think it`d be both a notification and a\nplace to see past announcements\nLike a one way chat.")
contentText.addAttribute(.baselineOffset, value: 10, range: NSRange(location: 0, length: 5))
contentText.addAttribute(.baselineOffset, value: -10, range: NSRange(location: 85, length: 20))
Result:
"I see
I'd think it`d be both a notification and a
place to see past announcements
Like a one way chat."
https://stackoverflow.com/a/55876401/4683601

Related

Autolayout constraints in TextField of an outlineView

last night a have an autolayout issue. I was googling and try to find something similar in SO. Even the apple doc doesn't point me to the right direction. Maybe my search terms are completely wrong.
Maybe you guys can bring some light into my darkness.
I added a NSOutlineView in storyboard and added some constraints to the NSTableCellView. As you can see, i added a trailing space to Superview of 50:
My example code adds some foo's and bar's into the outlineView by identifyer:
func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
if tableColumn?.identifier == "NAME_COLUMN" {
cell = outlineView.makeViewWithIdentifier("NAME_COLUMN", owner: self) as NSTableCellView
cell.textField!.stringValue = "foo"
cell.textField!.editable = true
cell.textField!.delegate = self
} else
if tableColumn?.identifier == "VALUE_COLUMN" {
cell = outlineView.makeViewWithIdentifier("VALUE_COLUMN", owner: self) as NSTableCellView
cell.textField!.stringValue = "bar"
cell.textField!.editable = true
cell.textField!.delegate = self
}
return cell
}
But the trailing space will not show up in my running application!
I even try to set the cell display:
cell.needsDisplay = true
cell.needsLayout = true
cell.needsUpdateConstraints = true
or - according to someone on the internet - add "requiresConstraintBasedLayout":
class func requiresConstraintBasedLayout() -> Bool {
return true
}
but all without luck. The trailing space do not appears and the bar's on the right side border looks awful.
How do i use a TableViewCell inside a OutlineView with a trailing space?
Thanks a lot for any kind of hint.
ps

Replacement for sizeWithFont:ForWidth:lineBreakMode:

Many of the methods I had been using to determine layout of printed strings for creation of complex pdf documents have been deprecated in iOS7. Documentation calls out the same method to use as replacement for all the sizeWithFont methods that are deprecated:
boundingRectWithSize:options:attributes:
That is fine for sizeWithFont:ConstrainedTosize:lineBreakMode but what if I want my string on one line only? I don't know what to use for max height so I do not have a rect to hand over as a value for the first parameter.
Here is what I have when limiting to a given size.
CGFloat maxHeightAllowable = _maxHeight;
CGSize issueTitleMaxSize = CGSizeMake(_issueListTitleColWidth - (kColumnMargin *2), maxHeightAllowable);
NSDictionary *issueTitleAttributes = [NSDictionary dictionaryWithObjectsAndKeys:_bodyFont, NSFontAttributeName, nil];
CGRect issueTitleRect = CGRectIntegral([issueTitleText boundingRectWithSize:issueTitleMaxSize options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) attributes:issueTitleAttributes context:nil]);
CGSize issueTitleSize = issueTitleRect.size;
How would I use this same method if I don't know the maxHeight, or actually, height for one line is exactly what I am trying to find out?
I see why they are pushing towards compatibility for the NSAttributed strings and auto layout but why deprecate these? The replacement, in my case, now takes 4 or 5 steps where it used to be 1 or 2.
Using the lineHeight property of font, as suggested by Mr T, I made these methods in a category that greatly simplifies my replacement.
#import "NSString+SizingForPDF.h"
#implementation NSString (SizingForPDF)
-(CGSize)integralSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)maxSize
{
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, nil];
CGRect rect = CGRectIntegral([self boundingRectWithSize:maxSize options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) attributes:attributes context:nil]);
return rect.size;
}
-(CGSize)integralSizeWithFont:(UIFont *)font maxWidth:(CGFloat)maxWidth numberOfLines:(NSInteger)lines
{
if (lines == 0) {
lines = 1;
}
NSDictionary *attributes = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
CGFloat height = font.lineHeight * lines;
CGSize maxsize = CGSizeMake(maxWidth, height);
CGRect rect = CGRectIntegral([self boundingRectWithSize:maxsize options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingTruncatesLastVisibleLine|NSStringDrawingUsesFontLeading attributes:attributes context:nil]);
return rect.size;
}
#end
If you were just looking for the height of one line, couldn't you just use your font's lineHeight property? I use that to set the height of my labels or properly anticipate height of elements without any issues. I'm not certain if pdf documents are different in this regard.
Additionally, I believe those functions were deprecated because that series of NSString+UIKit functions (sizeWithFont:..., etc) were based on the UIStringDrawing library, which wasn't thread safe. If you tried to run them not on the main thread (like any other UIKit functionality), you'll get unpredictable behaviors. In particular, if you ran the function on multiple threads simultaneously, it'll probably crash your app. This is why in iOS 6, they introduced a the boundingRectWithSize:... method for NSAttributedStrings. This was built on top of the NSStringDrawing libraries and is thread safe.
On that note, if you were only supporting iOS 6 and iOS 7, then I would definitely change all of your NSString's sizeWithFont:... to the NSAttributeString's boundingRectWithSize. It'll save you a lot of headache if you happen to have a weird multi-threading corner case! Here's how I converted NSString's sizeWithFont:constrainedToSize::
What used to be:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
CGSize size = [text sizeWithFont:font
constrainedToSize:(CGSize){width, CGFLOAT_MAX}];
Can be easily replaced with:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
NSAttributedString *attributedText =
[[NSAttributedString alloc]
initWithString:text
attributes:#
{
NSFontAttributeName: font
}];
CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX}
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
CGSize size = rect.size;
Please note the documentation mentions:
In iOS 7 and later, this method returns fractional sizes (in the size
component of the returned CGRect); to use a returned size to size
views, you must use raise its value to the nearest higher integer
using the ceil function.
So to pull out the calculated height or width to be used for sizing views, I would use:
CGFloat height = ceilf(size.height);
CGFloat width = ceilf(size.width);

How to calculate the height of NSAttributedString, given width and number of lines?

I want to display 3 lines of NSAttributedString. Is there a way to figure out the needed height, based on width and number of lines?
And I don't want to create a UILabel to do the size calculation, since I want the calculation to be done in background thread.
I wonder why this is still unanswered. Anyhow, here's the fastest method that works for me.
Make an NSAttributedString Category called "Height". This should generate two files titled "NSAttributedString+Height.{h,m}"
In the .h file:
#interface NSAttributedString (Height)
-(CGFloat)heightForWidth:(CGFloat)width;
#end
In the .m file:
-(CGFloat)heightForWidth:(CGFloat)width
{
return ceilf(CGRectGetHeight([self boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading
context:nil])) + 1;
}
Here's what's happening:
boundRectWithSize:options:context get's a rect constrained to a width you pass to the method. The NSStringDrawingUsesLineFragmentOrigin option tells it to expect multiline string.
Then we fetch the height parameter from that rect.
In iOS 7, this method returns decimals. We need a round figure. ceilf helps with that.
We add an extra unit to the returning value.
Here's how to use it
NSAttributedString *string = ...
CGFloat height = [string heightForWidth:320.0f];
You can use that height for your layout computations.
The answer by #dezinezync answers half of the question. You'll just have to calculate the maximum size allowed for your UILabel with the given width and number of lines.
First, get the height allowed based on number of lines:
let maxHeight = font.lineHeight * numberOfLines
Then calculate the bounding rect of the text you set based on the criteria:
let labelStringSize = yourText.boundingRectWithSize(CGSizeMake(CGRectGetWidth(self.frame), maxHeight),
options: NSStringDrawingOptions.UsesLineFragmentOrigin,
attributes: [NSFontAttributeName: font],
context: nil).size
There is a method in TTTAttributedLabel called
+ (CGSize)sizeThatFitsAttributedString:withConstraints:limitedToNumberOfLines:
Basically,this method use some Core Text API to calculate the height, the key function is
CGSize CTFramesetterSuggestFrameSizeWithConstraints(
CTFramesetterRef framesetter,
CFRange stringRange,
CFDictionaryRef __nullable frameAttributes,
CGSize constraints,
CFRange * __nullable fitRange )
which I think ,is also used by
- (CGRect)textRectForBounds:limitedToNumberOfLines:
this is a workaround and I think there are better way...
static UILabel *label;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
label = [UILabel new];
});
label.attributedText = givenAttributedString;
CGRect rect = CGRectMake(0,0,givenWidth,CGFLOAT_MAX)
CGFloat height = [label textRectForBounds:rect
limitedToNumberOfLines:2].size.height;

How do you get the line height of the text in a Spark Label?

How do you determine the line height (in pixels) of the text in a Spark Label?
keyle's answer will only give you the height of the text, not the actual 'lineHeight' style. You can easily get that style like this:
myLabel.getStyle("lineHeight");
The problem is that this can return a relative value (a percentage) or an absolute value (in pixels). The default - if no lineHeight was explicitely set - is "120%".
So here's how we can get the value in pixels in both cases:
var lineHeightStyle:* = myLabel.getStyle("lineHeight");
//its already a value in pixels
if (lineHeightStyle is Number) var lineHeight:Number = lineHeightStyle;
//it's a relative value: let's calculate
else {
var lineMetrics:TextLineMetrics = myLabel.measureText(myLabel.text);
//get the numeric value from the string and divide it by 100
var ratio:Number = int(lineHeightStyle.match(/\d+/)[0]) / 100;
lineHeight = lineMetrics.height * ratio;
}
Have you tried the following?
var lineMetrics:TextLineMetrics = label.measureText(label.text);
var result:Number = lineMetrics.height;

How to automatically set TextField()'s width

I'm trying to set the width of a Textfield() object based on it's string content that I have set-
Is there a way to dynamically set this once the string has been sent to the object?
I have:
var t1:TextField = new TextField()
t1.x = stage.stageWidth / 2;
t1.y = stage.stageHeight / 2;
t1.text = "some string that i would want to render";
t1.textColor = 0x000000;
t1.cacheAsBitmap = true;
addChild(t1);
Thanks for any suggestions...
jml
TextField.autoSize?
edit:
You should read the documentation correctly, it's a member variable that actually needs to be set. I'll give you a quick example on how this works:
var tf:TextField = new TextField();
tf.text = 'Some text.';
tf.autoSize = TextFieldAutoSize.LEFT;
tf.x = ( stage.stageWidth - tf.width ) / 2;
tf.y = ( stage.stageHeight - tf.height ) / 2;
Alternatively you can also align the text field first and use TextFieldAutoSize.CENTER to keep it aligned in the center.
t1.text = "Some text";
t1.width = t1.textWidth + 5;
t1.height = t1.textHeight + 5;
Why the + 5? Because Adobe sucks and adds an internal gutter around your stuff. Per the docs this is supposed to be 2px per side, but it's actually slightly more, so you add another +1 for good measure.
you could try using the getCharBoundaries() method (it returns a rectangle surrounding a character at a specified index). Use that to get the rectangle from the first and last chars and set the width to the difference of those rectangles.
Pretty convoluted, there's got to be a better way, but if not this should work.

Resources