8

I am getting wrong height for an UILabel if I use NSAttributedString that has custom kern and lineSpacing.

Here is how I set the custom kern and line spacing:

override func viewDidLoad() {
    super.viewDidLoad()

    let shortText = "Single line"
    self.label.attributedText = self.getAttributedText(text: shortText, kern: 0.2, lineSpacing: 8)
    self.label2.attributedText = self.getAttributedText(text: shortText, kern: 0, lineSpacing: 8)
}

private func getAttributedText(text: String, kern: CGFloat, lineSpacing: CGFloat) -> NSAttributedString {
    let attributedString = NSMutableAttributedString(string: text)

    let style = NSMutableParagraphStyle()
    style.lineSpacing = lineSpacing

    let attributes: [NSAttributedStringKey : Any] =
        [.paragraphStyle : style,
         .kern: kern]

    attributedString.addAttributes(attributes,
                                   range: NSMakeRange(0, attributedString.length))

    return attributedString
}

And here is what I get:

Result

The first label (the one that has custom kern), has its height wrong. It's exactly 8 points taller than it should be - that's the custom line height that I am using.

This only happens for single line labels. If I use text that is on a couple of lines, it works as expected.

Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
itskoBits
  • 435
  • 4
  • 13

1 Answers1

2

This is a bug with NSAttributedStringKey.kern. As a workaround, you can calculate the number of lines of your UILabel with the suggestions in this answer. If it has one line only, set lineSpacing to 0.

private func getAttributedText(text: String, kern: CGFloat, lineSpacing: CGFloat) -> NSAttributedString {
    let attributedString = NSMutableAttributedString(string: text)

    let font = UIFont.systemFont(ofSize: 16)

    let attributes: [NSAttributedStringKey : Any] = [.kern: kern,
                                                     .font: font]

    attributedString.addAttributes(attributes, range: NSMakeRange(0, attributedString.length))

    let maxSize = CGSize(width: [custom width], height: CGFloat.greatestFiniteMagnitude)
    let sizeOfLabel = attributedString.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)

    if sizeOfLabel.height > font.lineHeight {
        let style = NSMutableParagraphStyle()
        style.lineSpacing = lineSpacing

        attributedString.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, attributedString.length))
    }

    return attributedString
}
Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
  • This workaround might work most of the time. It has a performance penalty as you need to calculate the height of each UILabel at least twice. In my case the initialization of that string happens long before the layout pass, which means I cannot easily pass the width of that UILabel. – itskoBits Feb 16 '18 at 14:44