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

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:

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:

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:

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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 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.

to nestle under the arm of the in . For a 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.

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.

NSAttributedString is great because it’s the result of years of research that have shown coupling between data and presentation to be ideal. — Mark Adams (@hyperspacemark) March 14, 2015

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:

Creating a basic attributed string let str = NSAttributedString( string: "hello, world", attributes: [ NSFontAttributeName: UIFont.systemFontOfSize(24), NSForegroundColorAttributeName: UIColor.redColor(), NSBackgroundColorAttributeName: UIColor.blueColor(), ]) 1 2 3 4 5 6 7 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.

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.

pass 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:

Converting Adobe tracking values to UIKit points let tracking = myFont.pointSize * adobeTrackingValue / 1000.0 1 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 = BONChain.new .adobeTracking(300) // Adobe tracking .fontNameAndSize(@"Avenir-Book", 18.0f) .string(myString) .attributedString; NSAttributedString *string2 = BONChain.new .tracking(0.6) // UIKit tracking .fontNameAndSize(@"Avenir-Book", 18.0f) .string(myString) .attributedString; 1 2 3 4 5 6 7 8 9 10 11 NSAttributedString * string1 = BONChain . new . adobeTracking ( 300 ) // Adobe tracking . fontNameAndSize ( @"Avenir-Book" , 18.0f ) . string ( myString ) . attributedString ; NSAttributedString * string2 = BONChain . new . tracking ( 0.6 ) // UIKit tracking . fontNameAndSize ( @"Avenir-Book" , 18.0f ) . string ( myString ) . attributedString ;

Inline Images

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.

Creating a text attachment in Swift let attachment = NSTextAttachment() attachment.image = myImage attachment.bounds = CGRect(origin: CGPoint.zero, size: myImage.size) let attributedString = NSAttributedString(attachment: attachment) 1 2 3 4 let attachment = NSTextAttachment ( ) attachment . image = myImage attachment . bounds = CGRect ( origin : CGPoint . zero , 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] 1 [ 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 = BONChain.new.color([UIColor darkGrayColor]); BONChain *baseImageChain = BONChain.new.baselineOffset(-10.0f); 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 withSeparator:BONChain.new.string(BONSpecial.emSpace).text]; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 NSString * text = @"This string is separated by images and no-break spaces." ; NSArray * words = [ text componentsSeparatedByString : @" " ] ; BONChain * baseTextChain = BONChain . new . color ( [ UIColor darkGrayColor ] ) ; BONChain * baseImageChain = BONChain . new . baselineOffset ( - 10.0f ) ; 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 withSeparator : BONChain . new . string ( BONSpecial . emSpace ) . text ] ;

Number Spacing

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.

or figures all take up the same amount of horizontal space. A is the same width as an , 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:

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( font.fontName, font.pointSize, nil) let features = CTFontCopyFeatures(coreTextFont) 1 2 3 4 5 6 let font = UIFont . systemFontOfSize ( 17 ) let coreTextFont = CTFontCreateWithName ( font . fontName , font . pointSize , nil ) 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 } ] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 "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.

Create a UIFont with tabular figures 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) 1 2 3 4 5 6 7 8 9 10 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 = BONChain.new .fontNameAndSize(@"Archer-Book", 18.0f) .string(someString) .figureSpacing(BONFigureSpacingTabular) .attributedString; 1 2 3 4 5 NSAttributedString * attributedString = BONChain . new . fontNameAndSize ( @"Archer-Book" , 18.0f ) . string ( someString ) . figureSpacing ( BONFigureSpacingTabular ) . attributedString ;

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.

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:

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 = BONChain.new .fontNameAndSize(@"Archer-Book", 18.0f) .string(someString) .figureCase(BONFigureCaseOldstyle) .attributedString; // Both! attributedString = BONChain.new .fontNameAndSize(@"Archer-Book", 18.0f) .string(someString) .figureCase(BONFigureCaseOldstyle) .figureSpacing(BONFigureSpacingTabular) .attributedString; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // Figure Spacing NSAttributedString * attributedString = BONChain . new . fontNameAndSize ( @"Archer-Book" , 18.0f ) . string ( someString ) . figureCase ( BONFigureCaseOldstyle ) . attributedString ; // Both! attributedString = BONChain . new . fontNameAndSize ( @"Archer-Book" , 18.0f ) . string ( someString ) . figureCase ( BONFigureCaseOldstyle ) . figureSpacing ( BONFigureSpacingTabular ) . attributedString ;

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.