UILabel vertical text alignment to match Figma design

214 Views Asked by At

I have previously solved the challenge of creating a pixel perfect representation of a Figma design, but aligning text proved surprisingly difficult. I ended up with a solution that worked, but I was not too happy about it. Now I am at the challenge again and hope to get some input to solve this in a better way.

I am surprised and almost shocked it is this difficult, let me explain.

Requirements

  1. I should be able to style the UILabel exactly as text is styled in Figma. This should be fairly easy as Figma actually outputs the NSAttributed string properties to use, but as we'll see it does not result in accurate design.
  2. When I place a UILabel at the same distance from the top of the screen as a text box is placed in Figma. The vertical alignment or baseline of text should be exactly the same on the device/simulator as it is in Figma.
  3. The UILabel should support Dynamic Type and allow text to grow according to the user's text size settings in the device settings. With default dynamic type settings text should look exactly as in Figma.

The problem

To the best of my knowledge it is simply not possible to make a vanilla UILabel behave as I have outlined above. This is after spending about a week exploring what others have done and trying out many different approaches my self. I am happy to be proved wrong.

The main issue is that a text box in Figma and a UILabel simply does not layout text in the same way as soon as the text style being used modifies the line height of the base font. This happens as soon as you add UX to the project. They will start fiddling around with text styles, adjusting line heights, kern, weights etc.

Any adjustment to line height of a font in Figma moves you into NSAttributedString territory and Figma themselves suggest you express this using lineHeightMultiple. The problem is as soon as you start describing a font with lineHeightMultiple the font no longer lays out similar to Figma and I have been unable to find an alternative approach, with the requirements I outlined above.

Figma has a very nice piece on their recent changes to text layout here.

The short version is that text is now laid out and centered in its text box. This is how a UILabel normally behaves as long as you do not adjust lineHeightMultiple or lineSpacing, which are the two main ways of controlling the line spacing of a font.

The following image should illustrate the issue: enter image description here

To the left you have the design in Figma. I am using Verdana, Size 16, Line hight 150%. The greyish background box next to that is Figma's own suggestion on how to achieve this layout. In the middle I am using the lineHeightMultiple approach, to the right the lineSpacing approach. As you can see there are issues with both approaches.

lineHeightMultiple

Even though this approach accurately reflects the distance between two lines of text, the text it self is not laid out the same inside the text box / UILabel. As noted earlier Figma lays out text in the middle of the text box, while UILabel, when using lineHightMultiple adds the additional space above each line of text. This means text is "pushed" down inside the UILabel and is vertically aligned off-center.

lineSpacing

Also with lineSpacing we get the accurate distance between two lines of text, but here spacing above and below the first and last line of text is left out, meaning the UILabel ends up becoming smaller than its corresponding text box in Figma.

See also a comparison here

Both approaches results in layout that does not accurately reflect the design in Figma, unless you introduce additional measures.

Solution

Both of the above approaches can be solved, but the solution is not pleasing and comes their own set of issues.

lineHeightMultiple

Since the issue is that text is aligned off-center, compared to Figma, a solution here is to embed the UILabel in a container view, and adjust its y-axis position to make the text centered in the container view. Since the additional line height is added above the line of text, we can calculate the offset as such:

let yOffset = ((lineHeightMultiple * lineHeight) - lineHeight) * 0.5

Where lineHeight if the line height of the font. If you are supporting dynamic type, you need to adjust this whenever preferredContentSizeCategory changes, by hooking into the traitCollectionDidChange method of a UIVIew and use the lineHeight of the scaled font.

With that I end up with the following, where the blue color represents the container, that I will then use for layout, with the text label (red) off-set inside it.

enter image description here

This is the solution I have been using my self in the past, but apart from being far from appealing, it also does not solve well the challenge of adjustsFontSizeToFitWidth style labels. In this case UIKit will shrink the font to fit the space available, however the actual size of the shrunk font is not easily available making it difficult to adjust the Y-offset correctly.

lineSpacing

The solution to the line spacing approach is similar, but requires adding the correct amount of padding between the container view and internal label to achieve the same effect, the centering of text in the container. I will not elaborate further on that, but simply link to a great post on the topic here by Mick F.

Alternative Solutions

Using maximumLineHeight and minimumLineHeight

Another approach I have seen mentioned in a great post by Geri Borbás here is forcing the line height of the label to a fixed size by calculating and setting maximumLineHeight and minimumLineHeight to the same size. However this approach does not support Dynamic Type, but could be a solution if you don't need to support font scaling.

Using baseLineOffset

At face value it sounds like baseLineOffset would be the way to go, but in my experience I have not able to make it give accurate results. It seems the value of baseLineOffset is added to the lineHeight of the font which again, does not give the expected result.

Help?

Currently this is where I am add. I have two solutions as outlined above, that both will take me a lot of the way to solve the problem and this is probably where I'll end again. However I am surprised it turns out to be this difficult to do something that should be fairly common to most app developers. Are there other solutions out there short of diving into CoreText that I am missing?

0

There are 0 best solutions below