Zorayr
Zorayr

Reputation: 24962

How to set line height for a single line text in SwiftUI?

Currently, I have been using .lineSpacing(...), but this only works for multi-line text.

/// Sets the amount of space between lines of text in this view.
///
/// - Parameter lineSpacing: The amount of space between the bottom of one
///   line and the top of the next line.
@inlinable public func lineSpacing(_ lineSpacing: CGFloat) -> some View

What this means is that it's harder for me to translate fonts exactly from sketch/figma, and I need to play around with the padding to get it right. Here is an example that shows this:

VStack {
    // Line spacing is ignored.
    Text("Hello, World!")
        .background(Color.green)
        .lineSpacing(50)

    Spacer()

    // Line spacing is correct.
    Text("Lorem ipsum is placeholder text commonly used in the graphic, print, and publishing industries for previewing layouts and visual mockups.")
        .background(Color.green)
        .lineSpacing(50)
}

Upvotes: 28

Views: 54277

Answers (8)

davidev
davidev

Reputation: 8547

I also used Ole-Kristian answer to map exact font styles from Figma into SwiftUI. However, there was still a minimal difference in the size when I measured the actual height of the font in a screenshot afterwards. I investigated and it turned out that you need to take the different device scale ratio into account, because line heights with long decimal places cannot be displayed correctly. However, the padding is calculated with these exact numbers. I came up with the following solution:

// Adjusts the line height to achieve pixel-perfect designs, similar to designs created in Figma.
// This function takes into account the screen scale factor to ensure that the line height aligns correctly
// with the pixel grid of the device's display.
private func adjustLineHeight(_ lineHeight: CGFloat) -> CGFloat {
  // Get the screen scale factor (1x, 2x, 3x, etc.)
  let scale = UIScreen.main.scale

  switch scale {
  case 1.0:
    return lineHeight.rounded(.up)
  case 2.0:
    let fractionalPart = lineHeight - floor(lineHeight)
    return fractionalPart >= 0.5 ? lineHeight.rounded(.up) : round(lineHeight * 2) / 2
  default:
    let fractionalPart = lineHeight.truncatingRemainder(dividingBy: 1.0)

    if fractionalPart <= 0.3333 {
      return floor(lineHeight) + 0.3333
    } else if fractionalPart <= 0.6666 {
      return floor(lineHeight) + 0.6666
    } else {
      return ceil(lineHeight)
    }
  }
}

The modifier then uses this function when calculating the padding:

public struct LineHeightModifier: ViewModifier {
  let font: UIFont
  let lineHeight: CGFloat

  public func body(content: Content) -> some View {
    content
      .font(Font(font))
      .lineSpacing(lineHeight - font.lineHeight)
      .padding(.vertical, (lineHeight - adjustLineHeight(font.lineHeight)) / 2)
  }
}

Upvotes: 0

Klemen Košir
Klemen Košir

Reputation: 87

Actually what worked for me was (based on the answer by Ole-Kristian)

import SwiftUI

struct FontWithLineHeight: ViewModifier {
    let font: UIFont
    let lineHeight: CGFloat

    func body(content: Content) -> some View {
        content
            .font(Font(font))
            .lineSpacing((lineHeight - font.lineHeight)/2)
            .padding(.vertical, (lineHeight - font.lineHeight))
    }
}

extension View {
    func fontWithLineHeight(font: UIFont, lineHeight: CGFloat) -> some View {
        ModifiedContent(content: self, modifier: FontWithLineHeight(font: font, lineHeight: lineHeight))
    }
}

...very similar to the other answers with values for padding and lineSpacing reversed.

This was the only way I could get the bottom 'padding' to be the same as Figma.

Upvotes: 3

hstdt
hstdt

Reputation: 6261

iOS 17+ with UIKit NSMutableParagraphStyle

let paragraphStyle = NSMutableParagraphStyle()
// iOS 15/16 seems not working
paragraphStyle.maximumLineHeight = 21
paragraphStyle.minimumLineHeight = 21

var attributedString = AttributedString(text)
attributedString.paragraphStyle = paragraphStyle

Text(attributedString)

Upvotes: -1

Ole-Kristian
Ole-Kristian

Reputation: 701

Based on the answer by Dan Hassan I made myself a ViewModifier to do this, and it looks like it works as intended

import SwiftUI

struct FontWithLineHeight: ViewModifier {
    let font: UIFont
    let lineHeight: CGFloat

    func body(content: Content) -> some View {
        content
            .font(Font(font))
            .lineSpacing(lineHeight - font.lineHeight)
            .padding(.vertical, (lineHeight - font.lineHeight) / 2)
    }
}

extension View {
    func fontWithLineHeight(font: UIFont, lineHeight: CGFloat) -> some View {
        ModifiedContent(content: self, modifier: FontWithLineHeight(font: font, lineHeight: lineHeight))
    }
}

Upvotes: 44

mtzaquia
mtzaquia

Reputation: 96

I have been successfully using the following combination:

  • .frame(minHeight: lineHeight)
  • .lineSpacing(abs(designSystemFont.lineHeight - uiKitFont.lineHeight))

The abs may not be needed (assuming the system's line height is taller than the default one, but you never know if that may change, so...)

This allows me to handle for handling single-line text while respecting the design system requirements.

Upvotes: 1

Dan Hassan
Dan Hassan

Reputation: 171

For getting your text to match figma here's what finally worked for me. If for example your figma designs had a particular font of 16 pt with a line height of 32 pt:

let font = UIFont(name: "SomeFont", size: 16)!
return Text("Some Text")
    .font(.custom("SomeFont", size: 16))
    .lineSpacing(32 - font.lineHeight)
    .padding(.vertical, (32 - font.lineHeight) / 2)

In order to get the exact line spacing value we have to subtract our desired line height by the font's inherent line height but as you noted this will only take effect on multiline text and only between lines. We still need to account for the top and bottom padding of the text to match the desired line height so once again add the total line height minus the font's line height.

Upvotes: 17

ixany
ixany

Reputation: 6070

Xcode 12

Use the .leading() modifier to adjust line spacing.

Text("Hello\nworld").font(Font.body.leading(.tight))

Currently supported values: .tight, .standard and .loose.

Source: Apple Documentation

Upvotes: 12

Anon Anon
Anon Anon

Reputation: 121

You can just use .frame modifier and set the height.

Upvotes: -4

Related Questions