What If Your Designers Want a Baseline Grid on iOS?
In typography, there is the notion of a baseline grid: All the baselines of a text are aligned on invisible lines. Baseline alignment goes back at least 100 years: It is part of the Swiss Style/International Typographic Style.
Baseline alignment still goes strong today: On the web, CSS allows baseline alignment; on Android, baseline alignment is part of Material Design.
📱What about iOS?
Baseline alignment is not the default on iOS: A label takes the space needed for the content, the baselines do not lie on an invisible grid.
At one point, we were asked by our designers whether it would be possible to do baseline alignment on iOS. Let’s concentrate on UILabel
, as that is used to display text in most cases.
What complicates things is Dynamic Text. If an app uses that for accessibility, the designers have a choice of eleven predefined text styles (like headline, caption, …). The sizes of these text styles are not fixed, but change based on the user’s choice of the content size category. There are twelve content size categories: seven ranging from XS to XXL, and five even larger sizes for accessibility.
🥅 Goal
The goal of this article is to give an idea how to achieve baseline alignment for UILabel
, working for Dynamic Type (that means all 11 * 12 = 132 combinations), and for multi-line labels.
📕 Terminology
First, let’s align on the terminology how we use it in this article.
- The baseline is the imaginary line on which the characters rest.
- The ascender is the offset from the baseline to the font’s longest ascender, the topmost point of any character, including space for accents/umlauts.
- The descender is the offset from the baseline to the font’s longest descender.
- The line height is the height of a line of text, that is the ascender plus the descender.
- The leading is the extra distance between two lines of text. It is pronounced “ledding” and not “leeding”, as in hand typesetting it was done with strips of the metal lead.
😕 A Bit of Confusion
Some of the terminology is not used consistently.
- While the ascender for the fonts used in Dynamic Text includes space for accents and umlauts, it is often shown smaller in illustrations (Font handling, Apple Developer docs)
- The line height is often shown including the leading, but it does not include the leading on iOS (Font handling, Apple Developer docs)
- The leading is the extra distance between two lines of text, but sometimes (mainly in electronic typesetting) it is used to describe the full distance between two lines of text instead (Visual design typography, Apple Developer docs)
🔎 Implementation Details
Let’s see how to approach this in code.
🧱 Setup
We want to make sure our assumptions hold for all dynamic type fonts. There are 12 content size categories a user can choose.
We can use 11 different text styles for dynamic type.
With 12 content size categories and 11 text styles, we end up with 132 combinations for which we have to check everything works fine. Let’s build an array of those fonts.
To see whether the height of a label matches our expectations, we add a helper method to give us the height of a label for a given font, number of lines, and an optional line spacing:
As our goal is to place something on a grid, let’s add a helper method to have an easy way to round up a number to the next multiple of a given unit:
📐 Validate Assumptions on Font Metrics
The ascender is always a positive value:
The descender is always a negative value (as it goes in the opposite direction from the baseline in comparison to the ascender):
The line height is always the sum of the ascender and the negated descender:
☝️ Validate Assumptions for Single Line Labels
Contrary to what we may expect at first, the height of a label of a single line of text is not always equal to the line height of the font:
To get the height of the label, we have to round up the line height to the next “pixel”:
🎯 Reaching the Goal for Single Line Labels
In iOS’ Auto Layout, we can anchor to the first and the last baseline of a label via firstBaselineAnchor
/lastBaselineAnchor
. Except for a tiny bit of rounding up, the single line label has the same height as the line height.
If we have a top anchor that is on the grid, we can add a constraint that sets the first baseline of the label to that anchor plus the ascender rounded up to the grid size. That way, the baseline ends up on the grid, and as we rounded the ascender up, there is enough space for the ascender:
If we have a bottom anchor that should end up on the grid, we can add a constraint that sets the last baseline of the label plus the (negated) descender rounded up to the grid size to that anchor.
So now if the top anchor is on the grid, the two constants fix the baseline and the bottom anchor to the grid. This solves the issue for single line labels. For multi-line labels, we additionally need to make sure that the distance between two baselines is a multiple of the grid unit.
🖐 Validate Assumptions for Multi Line Labels
The multiple of the line height rounded up to the next pixel is not always the label height.
To calculate the correct height, we have to take the leading into account:
The distance between two baselines is font.lineHeight + font.leading
. To get a grid alignment for multi-line labels, we need to round that distance up to a multiple of the grid. So how can we add to the distance between two lines? lineSpacing
in an attributed text’s paragraph style sounds right.
Unfortunately it’s not that easy. Here is a test adding 1.5 to the line spacing of a two-line label. There are cases where the calculation is off:
The calculation only works if the leading is less than or equal to zero:
If the leading is positive, it turns out it is used as a minimum line spacing for dynamic type fonts! This is an inconsistency between the handling of negative and positive leadings. Examples:
- Leading -0.5 points, lineSpacing 1.5 points: 1 point between the lines
- Leading +0.5 points, lineSpacing 1.5 points: 1.5 points between the lines
- Leading +2 points, lineSpacing 1.5 points: 2 points between the lines
To increase the line height correctly for positive leadings, we have to add positive leadings to the line spacing.
Now we have figured out how to reliably increase the distance between two baselines. What’s left is to set that increase to the distance between two baselines is a multiple of the grid unit.
🎯 Reaching the Goal for Multi Line Labels
For a label, the default distance between the baselines is the line height plus the leading.
Our target distance between baselines is the default distance rounded up to the next multiple of the grid unit. As the constraints from the single line label handling above guarantee the first baseline to be on the grid, all the following lines will then be on the grid as well.
The line spacing we have to set is the difference between the two.
But as positive leadings act as minimum line spacing, we have to add those to the line spacing.
Now we can use this line spacing in a paragraph style, and instead of setting the text on the label, set the attributed text with that paragraph style.
🛠 Putting It All Together
The outcome of this experiment is that aligning to a baseline grid on iOS is possible.
You can find a playground with the code shown in this article, as well as a sample app with an implementation of a label view that aligns the text on the baselines hosted on GitHub here: https://github.com/mobimeo/ios-baseline-alignment. For production code, you should consider converting the assertions into unit tests, so if the behavior changes in future versions of iOS, you learn about it early enough.
The biggest learning for me was the different handling of leading for positive and negative values. Or maybe that I pronounced leading wrong for way too long. 😅