Core Text - NSAttributedString line height done right?
Asked Answered
C

10

65

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?

Crimson answered 15/11, 2011 at 15:35 Comment(1)
I couldn't figure out to get Times answer to get working. I did it that way.Conferral
C
12

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.

Crimson answered 17/11, 2011 at 6:15 Comment(3)
I'm sorry, could you expand on your code snippet a bit? What type of object is paragraphStyle? And what about lineHeight and floatValues? Thanks!Falcate
I got it to work with floatValues declared with CGFloat floatValues[4]; and paragraphStyles declared with CTParagraphStyleSetting paragraphStyle[3];Bucharest
Also Xcode recommended I (__bridge id)style in the addAttribute callBucharest
S
127

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))
Salim answered 2/5, 2013 at 12:21 Comment(9)
@Deco -- it's an NSMutableAttributedStringEmyle
lineHeightMultiple is also another nice option to use instead of lineSpacing - i typically set it to 1.2.Lempres
how to reduce line spacing below the default space. This method is only working to increase the height.Nowak
lineHeightMultiple can reduce the line height, by setting it to less than one. [style setLineHeightMultiple:0.8]Gaughan
Yes. But keep in mind that setLineHeightMultiple is not available on iOS, it's Mac OSX only.Salim
NSMutableParagraphStyle.lineHeightMultiple is iOS 6.0 and later.Cachexia
Yes... line spacing has worked for all my fonts until just now, thanks @Lempres for that extra bit.Tristatristam
Doesn't really explain how to exactly calculate line height, which is different than spacing between the lines.Manolete
@Nowak use a negative value for lineSpacingChiro
C
12

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.

Crimson answered 17/11, 2011 at 6:15 Comment(3)
I'm sorry, could you expand on your code snippet a bit? What type of object is paragraphStyle? And what about lineHeight and floatValues? Thanks!Falcate
I got it to work with floatValues declared with CGFloat floatValues[4]; and paragraphStyles declared with CTParagraphStyleSetting paragraphStyle[3];Bucharest
Also Xcode recommended I (__bridge id)style in the addAttribute callBucharest
P
9

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]
Prearrange answered 25/4, 2017 at 18:19 Comment(0)
H
8

You can set/update line spacing and line height multiple from storyboard as well as programatically.

From Interface Builder:

enter image description here

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
Hitoshi answered 11/10, 2017 at 10:46 Comment(0)
M
5

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
...
Manolete answered 8/8, 2019 at 11:0 Comment(0)
S
3

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.
Shitty answered 25/6, 2019 at 13:5 Comment(0)
G
2

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.

Gallic answered 18/7, 2018 at 18:45 Comment(2)
It took me a while to finally understand this approach. I think this will indeed work but there is a catch. For example: I set font size to 16. then I want to the line height to be 24 (multiplier 1.5). In this case I would set lineHeightMutliplier = 1.25 and lineSpacing as 16 * 0.25 = 4. Text should look correct for texts larger than 1 lline. (Where lineSpacing will be applied).Chuckle
For short text (only 1 line), since lineSpacing will not be applied, text will show ”Bottom alignment” since extra space (line height multiplier) will show at the top. I wonder if there is really a way to work this...Chuckle
W
2

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)
     }
}
Winonawinonah answered 10/5, 2020 at 7:26 Comment(0)
P
0

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
    }
Pneumothorax answered 12/2, 2016 at 9:13 Comment(0)
P
0

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://mcmap.net/q/23547/-how-can-i-add-different-line-spacing-for-different-parts-of-nsattributedstring

Precipitant answered 27/4, 2019 at 1:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.