A few months ago i decided to try my hand at doing some work in swift but i ended up yak shaving my way into finally understanding what's going on in textkit. One of the neat things i came across was this idea of chaining font descriptors to progressively activate opentype features on a font. Now that we're in full blown hack week at khan academy, i'm taking the time to use this code for a project i'm working on.

This isn't a particularly new idea, but inspired by alamofire's neat post about chaining vis-a-vis http requests and the current 'uproar' about opentype features, i thought i'd write up what i learned then. Plus it's cute because swift™ and also because chaining!

One of the things i was hoping to do was exploit as much of the power of Proxima Nova in an iOS app. Like most modern fonts, proxima includes a bevy of opentype features including true small caps, old style figures, stylistic sets and so forth. You can learn more about proxima or read its overview (pdf) which explains most everything you might want to turn on and off.

The neat thing about chaining font descriptors is that it gives you a very handy way to grab a UIFont that has the opentype features you want available. For instance, a font which has small caps and forced uppercase->small caps (i.e. SMCP & C2SC), you can do something like:

var styledFont: UIFont = UIFont(
        name: "ProximaNova-Regular",
        size: 14.0)!
        .fontWithSMCP()
        .fontWithC2SC()
        .fontWithONUM()

These are three of the most basic (and popular) kinds of opentype features on fonts, namely: Small Caps (SMCP), Caps to Small Caps (C2SC), and Oldstyle Figures (ONUM). I used these 4-letter feature abbreviations because they most closely map onto the features you end up seeing in font brochures and are the exact same features that type designers implement when they're writing up opentype code.

but... how? and what does this mean? Let's find out!

how fonts in ios work

in iOS, there's this idea of a font as a specific instance of a typeface at a specific size. Actually, the photo up above sort of explains the idea perfectly. A font in that case would be "10pt Garamond." If you were instantiating it, you might say

var oldFont: UIFont = UIFont(name:"Garamond", size:10.0)

and for any run of text in iOS, you are allowed to set this UIFont property however you like. If you want to have a run of text with lining figures followed by a run of text with old style figures, you need to do the equivalent of

let liningFont: UIFont = UIFont(name:"Garamond", size:10.0)
let osfFont: UIFont = UIFont(name:"Garamond-scOSF", size:10.0)

and whenever you need to use both of them, what you do is create and attributed string that contains a run of text whose font may be liningFont and then append a second run of text whose font is osfFont. In the abstract, i think this doesn't sounds totally crazy, but it is a bit verbose. the above example reads like this:

var liningRun = NSMutableAttributedString(string: "01234",
    attributes: [NSFontAttributeName: liningFont!])
var osfRun = NSAttributedString(string: "567890",
    attributes: [NSFontAttributeName: osfFont!])
liningRun.appendAttributedString(osfRun)

which results in something like

Attributed strings can set a bunch of attributes: its font (which we've just seen), the kerning or tracking for a region of text, its use of ligatures, its color, or any number of other attributes. The key is that these attributes are settings that you can apply to areas of text. But important to keep in mind is that these settings, while similar to a Font's settings, are different.

In contrast, switching a font's size, small caps, oldstyle figures, stylistic alternates and other opentype features require creating a new font object with those features enabled rather than specifying that feature as an attribute of a region inside an attributed string.

As an aside, i personally think it's weird that something like kerning or use of ligatures is assumed to apply equally to multiple fonts in the same attributed string but something like an opentype feature like old stlyle figures is assumed to be a feature on a font, but that's just the world we live in.

In core text and by proxy, text kit, there are two ways to create a font. The first is by creating a new UIFont with a font name and a font size. The second is by using a UIFontDescriptor.

Font descriptors are an oddly optimistic technology. You basically say something like "i want a font on this device that matches, as closely as possible these properties" and the system is responsible for returning a font which has as many of those attributes as possible. I'll talk later about why i think font descriptors are overly optimistic, but let's get to the codey parts.

futzing with descriptors

the typical approach to muddling around with fonts, in particular, activating opentype features, is something like what you see in the fantastic 2013 wwdc session "using fonts with text kit" session by ned holbrook:

func fontWithONUM(baseFont: UIFont) -> UIFont {
    let oDesc = baseFont.fontDescriptor()
    let features: [NSObject: AnyObject] = [
        UIFontDescriptorFeatureSettingsAttribute: [
            [
                UIFontFeatureTypeIdentifierKey:
                    kNumberCaseType,
                UIFontFeatureSelectorIdentifierKey:
                    kLowerCaseNumbersSelector
            ]
        ]
    ]
    let newDesc = oDesc.fontDescriptorByAddingAttributes(features)
    return UIFont(descriptor: newDesc, size: 0.0)
}

The idea is that you serialize your UIFont instance into a font descriptor, mutate that by adding new features and finally create a brand new Font seeded by that modified descriptor (using 0.0 as the font size preserves the descriptor's font size) Here is how you might make use of this code:

let myFont:UIFont = UIFont(name:"ProximaNova-Regular", size:14.0)!
let myFancyFont:UIFont = fontWithONUM(myFont)

And that's not wrong, per sé, but it's not exactly the nicest syntax for this sorta thing and you can imagine the dreaded lisp-style fingernailcuttings for a partucilurly extravagant font with many features. The point of this blog post, though, is to suggest that PERHAPS there is a nicer looking way of doing this. Let's see it!

chaining like a boss

Instead of passing in a font and getting back another font, why not actually just start by extending UIFont? We can do this by writing basically the EXACT same sort of code

extension UIFont {
    func fontWithFeature(key: AnyObject, value:AnyObject) -> UIFont {
        let originalDesc = self.fontDescriptor()
        let features:[NSObject: AnyObject] = [
            UIFontDescriptorFeatureSettingsAttribute: [
                [
                    UIFontFeatureTypeIdentifierKey: key,
                    UIFontFeatureSelectorIdentifierKey: value
                ]
            ]
        ]
        let newDesc = originalDesc.fontDescriptorByAddingAttributes(features)
        return UIFont(descriptor: newDesc, size: 0.0)
    }
}

Two changes here: first, i'm extending UIFont with this method. This means that any UIFont instance can now call this method to its heart's content.

But also notice that the substantive change that's happened here is that rather than hard-coding the values for UIFontFeatureTypeIdentifierKey and UIFontFeatureSelectorIdentifierKey i instead pass them directly into the method as its only arguments (i no longer have to pass in the font because it's available as self.) So for any UIFont instance, you can now imbue it with any set of features easily.

But instead of dealing with features abstractly, we'd like to at least have a way of talking about them without having to resort to actual key values like savages.

So we do something like this:

extension UIFont {

    // repeated from above
    func fontWithFeature(...) -> UIFont { ... }

    func fontWithSMCP() -> UIFont {
        return self.fontWithFeature(
            kLowerCaseType,
            value: kLowerCaseSmallCapsSelector)
    }

    func fontWithC2SC() -> UIFont {
        return self.fontWithFeature(
            kUpperCaseType,
            value: kLowerCaseSmallCapsSelector)
    }

    func fontWithONUM() -> UIFont {
        return self.fontWithFeature(
            kNumberCaseType,
            value: kLowerCaseNumbersSelector)
    }
}

If you're wondering what other options exist other than kNumberCaseType, you can find other available features by looking at the <ATS/SFNTLayoutTypes.h> header in xcode or, more legibly and with comments, the newly redesigned truetype reference manual. In particular, this wacky naming convention i've chosen for my methods is due to me consciously ignoring the ATS/AAT abstractions (which presumably exist as a result of the churn of font and type layout technologies GX/AAT/TTF/OTF tech during the late 90s and early aughts) in favor of OpentType tags.

Anyhow, with these three methods, you can do something along the lines of writing code like:

let styledFont: UIFont = UIFont(
        name: "ProximaNova-Regular",
        size: 14.0)! // explicitly unwrap since init -> UIFont?
        .fontWithSMCP()
        .fontWithC2SC()
        .fontWithONUM()

and that would give you a font that would turn "A small caps font with 1234567890" into:

gripes

I obliquely mentioned that i have 'mixed' feelings about font descriptors and it's mostly because they're explicitly unreliable and because they assume the existence of exhaustive font families, such that you'd be able to query for a font with some properties like number types or special characters and so forth.

The system is designed to let you ask, but makes no assurances you'll get what you're looking for which can make it difficult to distinguish errors in your own code and errors in the font provided. Case in point rdar://17624040 (closed as dupe of rdar://17624040), a bug in iOS' bundled version of Avenir Next that prevents SMCP and C2SC from activating even though the features are present in the font (or if queried via CTFontCopyFeatures)

Which is sort of disappointing because those sorts of silent errors make it harder to trust that the more ambitious family-querying and filtering qualities of Font Descriptors work at all. That said, this is what you've got to work with.

Since this was mostly an exploration of activating features, i thought i'd add in another method for selectively activating stylistic sets as a bonus for anyone that made it all the way to the end. Enjoy!

/**
pass in a positive integer to activate that stylistic set
or pass a negative integer to deactivate that set.
0 deactivates all features.
 */
func fontWithSS(stylistic_set:Int) -> UIFont {
    // this cheats a bit, assuming the enum values for the
    // stylistic sets stay numbered the same, but... oh well...
    switch stylistic_set {
    case 0:
        let ss = 0
        return self.fontWithFeature(
            kStylisticAlternativesType,
            value: ss)
    case 1...20:
        let ss = stylistic_set * 2
        return self.fontWithFeature(
            kStylisticAlternativesType,
            value: ss)
    case (-20)...(-1):
        let ss = (stylistic_set * -2) + 1
        return self.fontWithFeature(
            kStylisticAlternativesType,
            value: ss)
    default:
        // in the worst case, return the font
        return self
    }
}

epilogue

If you're actually going this route, it might not be totally crazy to actually create your own shell around UIFont rather than extending it. In particular, you'll end up doing something like this if you do end up supporting Dynamic Type with non-helvetica in your app.

Also, i'm totally open to the very likely idea that i have gotten something wrong here. Let me know if i've gotten anything wrong via twitter @nsfmc or via email, which you should be able to find over to the left. hit me up with your feedback, please! thanks for reading!