From the blog

iOS Typography: Stop Saying “No” to Designers

Update – November 2016

BonMot, the library mentioned in this post, has received a significant update, and the code samples presented here are no longer valid. Check out the blog post introducing BonMot 4, which includes a link to the migration guide.

Original Post:

At the fourth Raizlabs Lightning Talks, I described how you can stop saying “no” to designers who ask you to reproduce advanced typographic effects on iOS. Here is a more detailed description of the techniques from the talk, including a few extras that I had to cut for time.

How iOS Typography Usually Works

You know the drill: designers create their masterpiece, developers can’t (or don’t have time to) implement it all, we haggle over the details, and meet somewhere in the middle. But as I talked with Raizlabs designer Matt Lawson about his designs for the iOS app we were building for Undercover Tourist, I learned the reasoning behind each of the design decisions he made, and I didn’t want to ignore any of them. To that end, I set out to understand how to replicate, in iOS, all the advanced typographic effects that are possible in Adobe Illustrator and similar design tools. Instead of getting the designs “close enough,” we learned the components of iOS typography required to replicate the designs pixel-for-pixel. And now, we pass that learning on to you.

Designers: please show this post to your developer friends if they say that iOS typography is too complex. Developers: quit whining and build something beautiful!

TL;DR: BonMot

By the end of the project, we had managed to reproduce all the effects from the designs, but our visual styling code had grown sprawling and repetetive. As I began collecting my thoughts for this post, I decided to try to abstract away much of the complexity and confusion surrounding iOS typography, and make it easier to use. To that end, I created BonMot, an attributed string generation library for iOS. For each section below, I’ve included a brief description of how you can accomplish the same effect in BonMot. The BonMot examples are in Objective-C (for now), but all other code samples are in Swift.

Cap Height Alignment

The Ride Detail screen in the Undercover Tourist app, showing historical and predicted wait times.

When we first built the Wait Times widget in the Undercover Tourist app, something didn’t quite look right. It didn’t match the comp we were given by our designer. The 25 and the Minute label were perfectly aligned to each other’s top, the text itself was shifted a few pixels from the comp. The effect was pretty subtle, so I’ll illustrate it with an exaggerated example and a different font:

Two labels top-aligned with each other. This is possible with standard Auto Layout.
Two labels top-aligned with each other. This is possible with standard Auto Layout.

The labels’ tops are aligned, but the text within those labels starts some distance down from the top of the frame. This can make for poor visual alignment if the backgrounds of the labels are transparent, because the only visual reference you have for alignment is top of the characters themselves. We could have tweaked the vertical offset between the two labels until it looked right, but if the text size or font changed later, we would have to update this value again. Better would be a way to dynamically measure the top inset of both labels and set the vertical adjustment in code:

Two labels aligned to each other by cap height. The difference is subtle, but depending on the font, it can make the difference in successful execution of a careful design.
Two labels aligned to each other by cap height. The difference is subtle (only a few pixels off from the original example), but depending on the font, it can impact successful execution of a careful design.

Luckily, UIFont can help us out. It exposes several font metrics properties that we can use to solve this and other, similar problems. In case your typographic anatomy is a bit rusty, here are the basics:

  • baseline: the invisible line on which all the letters rest.
  • x-height: the height of a lowercase letter x. Used as a reference point for the tops of most lowercase letters.
  • cap height: the height of capital letters.
  • ascender: an element of a letter that extends above the x-height, such as the stem of lowercase h and k.
  • descender: an element of a letter that drops below the baseline, such as the stem of a lowercase p and q.

Those are what you would learn in Design 101, but UIFont interprets some of these properties slightly differently. This illustration shows what these properties of UIFont mean in the context of a UILabel:

This is how UIFont interprets these common typographic attributes. The top and bottom dotted lines represent the frame of a UILabel.

From the illustration, we can see that ascender is the distance from the baseline to the top of the label, and capHeight is the distance from the baseline to the tops of capital letters. Therefore, by subtracting ascender – capHeight, we will get the distance from the top of the label’s frame to the top of the capital letters, which is what we want in this case. So, we calculate that top inset of both labels, subtract one from the other, and set it as the constant of a constraint that is aligning the labels’ tops to each other.

// leftFont and rightFont are the fonts of the two labels

// Distance from baseline to top of label
let leftAscender = leftFont.ascender
let rightAscender = rightFont.ascender

// Distance from baseline to top of capital letters
let leftCapHeight = leftFont.capHeight
let rightCapHeight = rightFont.capHeight

// Distance from top of label to top of capital letters
let leftTopOfLabelToTopOfCaps = leftAscender - leftCapHeight
let rightTopOfLabelToTopOfCaps = rightAscender - rightCapHeight

// topConstraint is a constraint aligning the two labels
// together by the NSLayoutAttribute.Top attribute
topConstraint.constant = leftTopOfLabelToTopOfCaps - rightTopOfLabelToTopOfCaps

The great thing about this solution is that when your font size changes, you don’t have to tweak the values manually. Your measurements will continue to work as long as you recalculate them at run time. This is especially useful when using dynamic type, because as your fonts grow and shrink, their relative vertical alignment may shift. This technique also works if you need to align labels by x-height instead of by cap height: just substitute xHeight for capHeight in the above code snippet.

Vertical Alignment in BonMot

BonMot includes an NSLayoutConstraint subclass which makes it easy to achieve this effect, even in Interface Builder. It’s called BONTextAlignmentConstraint, and it uses Key-Value Observing to update its measurement whenever either of its items’ font property changes, which makes it ideal to use in conjunction with Dynamic Type.

Tracking & Kerning

Nearly every text label in the Undercover Tourist app has letter spacing, or tracking, applied. Tracking and kerning are two similar but distinct ideas that are often conflated, and as you’ll see later, Apple’s API for controlling them does little to alleviate the confusion. The distinction is important, however, so let’s review:

  • Kerning: per-character-pair spacing adjustments that are built into a font by the font designer. Designers go through hundreds of individual pairs of characters and adjust the space between them, so that they always look good when they appear together. It’s what allows the o to nestle under the arm of the T in To. For a way to lose friends fun exercise, look for bad kerning on signs and restaurant menus, particularly those using the Papyrus typeface. Improper kerning is humorously referred to as keming.
  • Tracking: the overall horizontal spacing of a piece of text. It is applied after any kerning has been applied, so the characters still have whatever even spacing the font’s designer imparted. Tracking can be both positive (looser text) or negative (tighter text). The default is zero, which means your text has no spacing adjustments applied other than kerning.
Kerning concerns the space between individual letter pairs. Tracking is the overall spacing between all the letters in a piece of text.
Kerning concerns the space between individual letter pairs. Tracking is the overall spacing between all the letters in a piece of text. Here, the first text has bad kerning, and the second has loose tracking.

On iOS, the way you control tracking and kerning is with NSAttributedString. NSAttributedString consists of a string of text and one or more attributes applied to one or more ranges within that text. If you’re familiar with CSS, it’s a bit like making a whole website using nothing but inline styles in <span> tags.

A quick crash course in NSAttributedString: you create an attributed string from a plain string and a dictionary of attributes. You can apply these attributes to the whole string, or just to part of the string using an range. For example, here’s how you would create a basic string with a 24-point red system font on a blue background:

let str = NSAttributedString(
    string: "hello, world",
    attributes: [
        NSFontAttributeName: UIFont.systemFontOfSize(24),
        NSForegroundColorAttributeName: UIColor.redColor(),
        NSBackgroundColorAttributeName: UIColor.blueColor(),

In iOS typography, tracking and kerning are both controlled with the NSKernAttributeName key. This is the source of the confusion I mentioned above: it’s not exactly clear from the documentation what to do if you want to control either parameter. The docs say:

The value of this attribute is an NSNumber object containing a floating-point value. This value specifies the number of points by which to adjust kern-pair characters. Kerning prevents unwanted space from occurring between specific characters and depends on the font. The value 0 means kerning is disabled. The default value for this attribute is 0.

And from the UILabel docs on the attributedText property:

To turn on auto-kerning in the label, set NSKernAttributeName of the string to [NSNull null].

The docs appear to be incorrect here. You actually don’t need to pass [NSNull null] (or NSNull() in Swift). If you just ignore NSKernAttributeName for an attributed string in a UILabel, you get the designer-approved kerning, even if you’re doing your own attributed string drawing in Core Graphics.

So to recap, here’s how you can achieve various tracking and kerning effects with UILabel and other UIKit classes that work with attributed strings:

  • Use the font’s built-in kerning: do nothing. This is the default behavior.
  • Disable the font’s built-in kerning: pass NSKernAttributeName: 0 in the attributed string’s attributes dictionary. Don’t do this unless you have a very good reason.
  • Adjust font tracking: pass a nonzero number for NSKernAttributeName.

One final note on tracking: NSKernAttributeName specifies an amount of tracking in UIKit points. However, designers working in Adobe Photoshop or Illustrator will be using the type panel’s tracking field, which specifies values in thousandths of an em (a typographic unit of size). Luckily, you can easily convert Adobe values to UIKit values with this formula:

let tracking = myFont.pointSize * adobeTrackingValue / 1000.0

Tracking & Kerning in BonMot

You can easily adjust tracking with either Adobe or UIKit values in BonMot:

NSAttributedString *string1 =
    .adobeTracking(300) // Adobe tracking
    .fontNameAndSize(@"Avenir-Book", 18.0f)

NSAttributedString *string2 =
    .tracking(0.6) // UIKit tracking
    .fontNameAndSize(@"Avenir-Book", 18.0f)

Inline Images

Images embedded inline in an attributed string
Images embedded inline in an attributed string

NSTextAttachment has been in OS X since the beginning, but it was new to UIKit as of iOS 7. It allows you to embed images or other types of data into attributed strings. Text attachments are typically embedded in the attributes dictionary of the special NSAttachmentCharacter (0xFFFC) using the attribute name NSAttachmentAttributeName. There is a convenience initializer that lets you create an attributed string with an attachment, but you can also manually create and attach your own NSTextAttachment objects. You can also attach things other than images, but that is outside the scope of this post. For more, refer to the documentation.

Though it is not required, I recommend setting the bounds property of text attachments. If you don’t, the string will resize the attachment, which may not be what you want.

let attachment = NSTextAttachment()
attachment.image = myImage
attachment.bounds = CGRect(origin:, size: myImage.size)
let attributedString = NSAttributedString(attachment: attachment)

In the screenshot above, at the end of each line, words that need to wrap to the next line pull their image attachments with them, rather than leaving them stranded. You can do this using the no-break space character, which looks like a normal space but it prevents automatic line breaks. Set up your text like this:

[image][no-break space]Some text goes here[normal space][image][no-break space]

Inline Images and Special Characters in BonMot

Use the BonSpecial class to easily access special spaces, dashes, and other obscure, pedant-pleasing characters. Here, we see images from an array being inserted between each word in a sentence. The images are separated from the words that follow them by no-break spaces, and each image-word pair is separated from the next by an em space.

NSString *text = @"This string is separated by images and no-break spaces.";
NSArray *words = [text componentsSeparatedByString:@" "];

BONChain *baseTextChain =[UIColor darkGrayColor]);
BONChain *baseImageChain =;
NSMutableArray *texts = [NSMutableArray array];

for (NSUInteger theIndex = 0; theIndex < images.count; theIndex++) {
    UIImage *image = images[theIndex];
    BONChain *chain = baseImageChain.image(image);
    chain.appendWithSeparator(BONSpecial.noBreakSpace, baseTextChain.string(words[theIndex]));

    [texts addObject:chain.text];

NSAttributedString *String = [BONText joinTexts:texts

Number Spacing

The same labels, using the system font, running on iOS 8 and iOS 9.
The same labels, rendered using the system font, running on iOS 8 and iOS 9.

Look at the above screenshots of the same app built with the iOS 8 SDK and the iOS 9 SDK, using the system font for the labels. See the difference? The numbers line up nicely in columns under iOS 8, but on iOS 9, they don’t. What’s going on here?

Numbers (also called “figures” in typography) can be spaced in two different ways:

  • Tabular or monospace figures all take up the same amount of horizontal space. A 1 is the same width as an 8, and this makes them line up perfectly in columns when stacked vertically.
  • Proportional figures have variable width, just like the letters in most typefaces. A 1 is narrower than an 8. While these figures don’t line up in columns, they tend to look better when used inline in a block of text, like this: “I think that 1984 was more significant than 2007 in terms of computing history.”

Either or both of these figure spacing styles can be embedded in a font. If both are present, a default is chosen, but it can be overridden.

In every version of iOS prior to 9, the default system font used tabular figures. This was great for cases where numbers were vertically stacked, but it could make the spacing look uneven when they were used inline in a run of text. Starting with iOS 9 and the new San Francisco system font, this default has been reversed: now, proportional figures are the default. To learn more about this and many other advanced features of the San Francisco typeface, and iOS typography in general, check out the excellent Introducing the New System Fonts talk from WWDC 2015.

Whatever the default is in the typeface of your choice, there may be times when you need to override it. For example, if you’re writing an app for iOS 9 and need to display numbers in a spreadsheet, tabular figures are a must. To change this and other advanced properties of a font, we use UIFontDescriptor.

UIFontDescriptor allows you to create new fonts by modifying attributes of other fonts. For example, you can create a bold or italic version of a given UIFont. In this case, we want a version of the default system font that uses tabular instead of proportional figure spacing.

To accomplish this, we are going to be using values from SFNTLayoutTypes.h. This obscure header is part of the Core Text framework. Core Text gives you full manual control over every aspect of text layout. That control is out of the scope of this post (it’s for making custom layout engines, not tweaked UI text), but we can dip our toes in enough to control some advanced aspects of our labels.

The controls provided via the UIFontDescriptor + SFNTLayoutTypes.h combination are similar to what you might find in the OpenType panel in a design tool such as Adobe Illustrator:

Your designer knows what happens when you click these buttons. Do you?
Your designer knows what happens when you click these buttons. Do you?

Designers love this panel, and after using these techniques in iOS, I hope that you will, too!

Before using these tools to control advanced OpenType features, you will need to make sure that your font supports them. This is important because some fonts exist in multiple versions, and the one you’re using in your app may be different than the one your designer has installed on their computer. If you’re sure that your font has the features you need, feel free to skip this section, but if not, here is how you can find out:

let font = UIFont.systemFontOfSize(17)
let coreTextFont = CTFontCreateWithName(
let features = CTFontCopyFeatures(coreTextFont)

features is a dictionary containing a full description of the font’s supported OpenType features. Here is a gist I created with the results of running this code on iOS 9 beta 5 (formatted as JSON for readability). It’s rather extensive, but I want to draw your attention to this section:

"CTFeatureTypeSelectors" : [
    "CTFeatureSelectorNameID" : -701,
    "CTFeatureSelectorIdentifier" : 0,
    "CTFeatureSelectorName" : "Monospaced Numbers"
    "CTFeatureSelectorNameID" : -702,
    "CTFeatureSelectorIdentifier" : 1,
    "CTFeatureSelectorName" : "Proportional Numbers"
    "CTFeatureSelectorDefault" : true,
    "CTFeatureSelectorName" : "No Change",
    "CTFeatureSelectorNameID" : -705,
    "CTFeatureSelectorIdentifier" : 4

We can see that the font supports both proportional and monospace numbers. To change which one is used, we create a new UIFont object, using a UIFontDescriptor to adjust these attributes.

let originalDescriptor = UIFont.systemFontOfSize(17).fontDescriptor()
let figureCaseDict = [
    UIFontFeatureTypeIdentifierKey: kNumberSpacingType,
    UIFontFeatureSelectorIdentifierKey: kMonospacedNumbersSelector,
let attributes = [
    UIFontDescriptorFeatureSettingsAttribute: [ figureCaseDict ],
let descriptor = originalDescriptor.fontDescriptorByAddingAttributes(attributes)
let newFont = UIFont(descriptor: descriptor, size: 0)

You can now use newFont just as you would any other UIFont, and you will get the number behavior that you expect. Notice kNumberSpacingType and kMonospacedNumbersSelector; these both come from SFNTLayoutTypes.h. I encourage you to browse that header yourself, as it contains all sorts of hidden typographic gems.

Figure Spacing in BonMot

NSAttributedString *attributedString =
    .fontNameAndSize(@"Archer-Book", 18.0f)

Figure Case

A typographic feature related to figure spacing is figure case. Some typefaces have numbers that look like lowercase letters, with figures like 3 and 9 dropping below the typographic baseline. These are called oldstyle figures, and they often look better inline with text, because they blend in better with lowercase letters. Lining figures, on the other hand, are the same size as uppercase letters, and they never drop below the baseline.

An example of lining and oldstyle figures. Lining figures are more typically used in headlines, while oldstyle is more appropriate for body text.
An example of lining and oldstyle figures. Lining figures are more typically used in headlines, while oldstyle is more appropriate for body text.

We needed to control the figure case for large numeric callouts in Undercover Tourist, because our main font, Archer, defaulted to oldstyle (lowercase) figures on iOS, and the designs called for lining (uppercase) figures in large callouts showing ride wait times:

The designs specified lining figures for the large numbers, but iOS defaulted to oldstyle figures.
The designs specified lining figures for the large numbers, but iOS defaulted to oldstyle figures.

Adjusting figure case is done using the same mechanism as adjusting figure spacing, above, except that you use the kNumberCaseType, kUpperCaseNumbersSelector, and kLowerCaseNumbersSelector constants to achieve the desired effect. And remember, you can combine figure case and figure spacing if your font supports it!

Figure Case in BonMot

// Figure Spacing
NSAttributedString *attributedString =
    .fontNameAndSize(@"Archer-Book", 18.0f)

// Both!
attributedString =
    .fontNameAndSize(@"Archer-Book", 18.0f)

If there’s a text effect that I didn’t cover, mention it in the comments! And if you’d like it included in BonMot, issues and pull requests are always welcome.


  1. Fantastic article! Never realized UIFont gave so much specific information, will definitely reference this.

    Also, is there a tweet button I’m missing to share articles?

  2. Thank you for a great article. I haven’t realized that it’s possible to change such things for a font. Played with kVerticalPositionType, kStylisticAlternativesType and they also could be useful. After your article, it’s become so simple to dig in font features 🙂

  3. This is a great article. I hope I find myself working on a project where I have a mandate to sweat so hard over the typographical details. It would be a real pleasure to dig into this stuff.

    One thing that seems tricky to me is that while Adobe tools give you a way to play with typographical settings, see the effect immediately, and learn what they mean visually, you’re stuck with descriptions and abstractions when you’re working with Apple’s API. Did you find or build any apps that let you dynamically play with all the attributed string and core text attributes live and see the results right in front of you?

  4. @soniccat: glad you found it useful! I had the same experience: I discovered SFNTLayoutTypes.h while looking for a way to change figure spacing, but once I’d found it, I spent some time just reading through all the options.

    @Alexis: Nothing comprehensive, but I did make a little GUI utility with some sliders to work out the conversion factor between Adobe and UIKit tracking. I don’t think I kept the source code. But BonMot could be the back end to a simple GUI tool for playing with fonts. Just make a UI that exposes a bunch of check boxes and text fields, and then have it spit out BonMot’s attributes dictionary for your perusal.

  5. Great article – do you have some advice regarding the use of the leading property. I don’t know if it has ever been very useful, but Apple describes it as:

    “The leading value represents the spacing between lines of text and is measured (in points) from baseline to baseline.”

    However, it seems that in iOS 9, the leading value for all fonts is 0 (zero), so are we not supposed to use it anymore – and if not, how do we calculate the standard distance between lines?

  6. @Stefan yep, that’s a tricky case, and I agree with this Stack Overflow comment: – you’re probably going to have to use Core Text to figure out the exact inset for the glyph in question at the font size you’re using. If you figure it out, feel free to open an issue against BonMot and I’ll consider adding to it to `BONTextAlignmentConstraint`!

  7. Thank you for this article. Our designer was pretty excited when I sent her the article 🙂
    Didn’t know about NSTextAttachment, we instead use a custom icon font for having icons in text. This is especially awesome as it allows to set the icon color to the text color. Are there any advantages of using NSTextAttachment?

    1. An icon font is certainly a good option if you need scalable vector images in text, and you’re right that you can recolor them. BonMot includes an image tinting utility, though it’s not yet as integrated as I’d like it to be, but it can do the same thing. As for advantages, you can only do single-color icons with icon fonts. NSTextAttachment lets you embed arbitrary images. It also lets you attach other media and file types, but that was out of the scope of this post.

  8. Great article! Would be even better if you dig into leading/line height more, as a designer, I want to know the system default line height and how to translate it to leading value so I can be more precise when designing the layout. The new SF UI font families provided a tracking value chart but didn’t provide a line height/leading value chart, which in cases like deciding a minimum table cell height would be helpful.

  9. Great article indeed! One thing I’ve been trying to do though is specify tracking without breaking dynamic font type support. Unfortunately my search on the web (that’s how I landed on this page) is not bringing any successful leads. Any suggestions?

  10. @shahla adjusting tracking shouldn’t break dynamic type, but since tracking is specified in points, you might need to grab the dynamic font, query its .pointSize, and calculate the tracking value as a multiple of the point size.

  11. Having been a designer for quite a while, and having been coding websites for a good portion of that time, all of these CSS properties are ones that I have been using for years. That is to say, for me specifically, there is nothing really new here. However, that does not invalidate what the author is saying.

    1. Yes, most of this is trivial in CSS, but annoyingly hard in iOS. That’s why we were avoiding some of the techniques for so long, and why I made BonMot, so we could use them as easily as font and text color.

    1. It’s a constant struggle. My last attempt to fully understand line height from Sketch/Photoshop to code ended in disappointment; there’s no consistency between the design tools and iOS, as far as I could tell. I’ve fallen back to trial and error 🙁

Leave a Reply

Your email address will not be published.