iOS App Development

Cap Height Alignment for iOS Auto Layout

By Mike Woods, Atimi Software Inc.



Many times graphic designs include the need to align text vertically at cap height (the top of the capitals) and baseline. For example, someone displaying a list of news articles might want to align the cap height of each article title and the baseline of the associated description with the thumbnail image (as shown in the figure below). iOS Auto Layout already supports baseline alignment for vertical (as well as horizontal) layout, but there is no built-in mechanism to deal with cap height. This article will show you how this can be done with a simple subclass of UILabel.


 

A Short lesson in iOS Typography

 

Before getting into implementation, let’s just run through the terminology that will be needed. The figure below is taken from Apple’s Text Programming Guide for iOS and shows the various vertical measures used in layout.



The important terms (along with their UIFont property names) for this discussion are:


• Line height (lineHeight) is the distance from one line of text to the next;
• Baseline is the vertical origin of the text, that is, the line upon which the characters stand;
• Ascent (ascender) is the distance from the baseline to the top of the text cell (including space for accents and the like);
• Descent (descender) is the distance below the baseline to the bottom of the text cell;
• Cap height (capHeight) is the distance from the baseline to the top of the capital letters.


Because of the additional space for accents and the like, a font’s ascent is typically higher than its cap height. Therefore, even when a UILabel (or other text view) is sized to hug its content, it will still have a gap between the top of the view and the top of the text.

Curiously, the diagram shows space below the descent for line gap (leading), but there is no corresponding property in UIFont—there is a “leading” property, but it is synonymous with the lineHeight property. Having surveyed all the fonts currently available on iOS, the line gap appears to be universally zero and so will be ignored for the rest of this discussion.


 

Changing the Meaning of  “Top”

 

It may come as a surprise, but when Auto Layout does alignment it does so against a view’s alignment rectangle and not simply its edges. By default, this alignment rectangle is the same as the view’s frame, but it can be different. For example, suppose a view displays an image within some custom border. It may be more appropriate to align layout against the image and not include the border.

We can use this feature on the labels for which we want cap height alignment. UIView defines a number of methods that can be used, but the simplest is alignmentRectInsets, which returns a UIEdgeInsets specifying the insets of the content with respect to the frame. The default implementation returns UIEdgeInsets.zero but if we subclass UILabel we can override it as follows:


override var alignmentRectInsets: UIEdgeInsets {
var insets = UIEdgeInsets.zero
insets.top = round(font.ascender - font.capHeight)
return insets

}

 

This simply defines the top margin to be the gap between the ascender and the cap height of the label’s font, causing Auto Layout to shift the meaning of “top alignment” into the view so it rests on the cap height instead.

This is all that is needed for basic functionality. Wherever cap alignment is required, just replace the UILabel with its subclass and Auto Layout top alignment will align with cap height instead—well, approximately so; see below for a more accurate calculation of cap height positioning.

This simply defines the top margin to be the gap between the ascender and the cap height of the label’s font, causing Auto Layout to shift the meaning of “top alignment” into the view so it rests on the cap height instead.

Of course, having to switch the class of the label whenever we want to change alignment could get annoying, particularly if a graphic design requires a dynamic switching from top to cap height. To support this, we can introduce a boolean property (alignCapHeight) to control the type of alignment. Our revised alignmentRectInsets now looks like:

 

override var alignmentRectInsets: UIEdgeInsets {
var insets = UIEdgeInsets.zero
if alignCapHeight {
insets.top = round(font.ascender - font.capHeight)

    }
return insets 

}
As the alignment method is only called during layout, we also need to ensure that Auto Layout is rerun whenever the property changes:

 

var alignCapHeight: Bool = false {
didSet {
setNeedsUpdateConstraints()
}
}

 

Working With IB

 

Great, but wouldn’t it be nice if the new vertical alignment showed up in IB? Well, that’s easily fixed: just add the @IBDesignable designation to the class definition as follows:


@IBDesignable
class CapHeightLabel: UILabel {

}

 

This tells IB that the class will co-operate with it to display correctly. In our case, this just means that IB will instantiate the label object and execute the alignmentRectInsets method so the positioning by IB’s Auto Layout matches the runtime.

Likewise, we can surface our alignCapHeight property to IB using @IBInspectable


@IBInspectable var alignCapHeight: Bool = false

 

This makes the property accessible in the attributes inspector (per the screenshot below). This is pretty cool—as you change the Align Cap Height attribute, IB will automatically update the layout so you see the impact in realtime.




A More Accurate Cap Height

 

In the code above, we approximated the cap height position by calculating the rounded difference between the ascender and the cap height. However, this is a very naive approach, given how text is actually rendered, and so the method is only accurate to ±1 point. To understand why this is so, and how to calculate a more accurate position, we need to work through how the text is actually rendered in a UILabel.

Suppose we have a label that is using a 24pt Helvetica font. That font will have the following metrics:




The fractional part of each number is important. When the font engine renders the glyphs it uses anti-aliasing to simulate the fractions of pixels, as shown in the enlarged image below. The other point to note is that glyphs are always laid out with respect to the baseline, and so the baseline is always on a pixel boundary.

Starting with lineHeight, we can see that it is the sum of the ascender and descender. However, as baseline is always on a pixel boundary, the actual line height of the rendered font must be an integral number of pixels. Also, as fractional parts are simulated through anti-aliasing, the line height cannot be rounded to the nearest pixel without risking the last partial pixel being clipped. Therefore, the calculated line height will be ceil(lineHeight).




This same principle holds for ascender and descender. Each may have a fractional part that needs to always round up to the next pixel. However, there is a catch, ceil(lineHeight) does not always equal ceil(ascender) + ceil(descender). Sometimes the calculated line height is one pixel shorter than the sum of the calculated parts. In such cases, the descender is given precedence on the basis that many common glyphs have descenders, whereas only a few diacritical marks touch the top of the ascender.

To find the position of the baseline within the label, we should move down by the calculated line height and then back up by the calculated descender. (Point of detail, for UIFont the descender is always expressed as a negative value because the Y access is “up”—all calculations therefore need to take this into account.)

With the baseline calculated, we can move up using the capHeight. However, in this case we actually do want to round to the nearest pixel as the visual position will depend on whether the fractional pixel is more or less than 50% opacity.

This gives us a more accurate adjustment calculation of:
 

ceil(lineHeight) - ceil(-descender) - round(capHeight)

 

However, there is one more consideration. All the metrics are in points but the rounding occurs at the pixel boundary. Therefore, the final calculation needs to handle the screen scaling as follows:

(ceil(font.lineHeight * scale) - ceil(-font.descender * scale) -
round(font.capHeight * scale)) / scale

 

Here is the final version of the code:.
  
override var alignmentRectInsets: UIEdgeInsets {
var insets = UIEdgeInsets.zero
if alignCapHeight, let scale = window?.screen.scale {
insets.top = (ceil(font.lineHeight * scale) - ceil(-
font.descender * scale) - round(font.capHeight * scale)) / scale
}
return insets
}

 

Final Considerations

 

What has been presented here works for UILabels, but the technique can also be applied to UITextField and UITextView. Also, it could be extended to support UILabels that use attributed strings (though it would be difficult to achieve a general solution for multi-line, multi-font labels).

It does not handle labels where the bounds do not hug the content. Although this can be calculated, there seems little practical use as it implies constraints that align both the content and the frame itself.

 

778-372-2800

 

info@atimi.com