Core Text - NSAttributedString line height done right?

NsattributedstringCore TextLine Spacing

Nsattributedstring Problem Overview


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?

Nsattributedstring Solutions


Solution 1 - Nsattributedstring

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))

Solution 2 - Nsattributedstring

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.

Solution 3 - Nsattributedstring

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]

Solution 4 - Nsattributedstring

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

Solution 5 - Nsattributedstring

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
...

Solution 6 - Nsattributedstring

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.

Solution 7 - Nsattributedstring

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.

Solution 8 - Nsattributedstring

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
    }

Solution 9 - Nsattributedstring

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

Solution 10 - Nsattributedstring

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)
     }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSchoobView Question on Stackoverflow
Solution 1 - NsattributedstringTiemeView Answer on Stackoverflow
Solution 2 - NsattributedstringSchoobView Answer on Stackoverflow
Solution 3 - NsattributedstringEnriqueView Answer on Stackoverflow
Solution 4 - NsattributedstringKrunalView Answer on Stackoverflow
Solution 5 - NsattributedstringMatej UkmarView Answer on Stackoverflow
Solution 6 - Nsattributedstringn13View Answer on Stackoverflow
Solution 7 - NsattributedstringwardwView Answer on Stackoverflow
Solution 8 - NsattributedstringMB_iOSDeveloperView Answer on Stackoverflow
Solution 9 - NsattributedstringEly DantasView Answer on Stackoverflow
Solution 10 - NsattributedstringRashid LatifView Answer on Stackoverflow