Easy, Beautiful Typography with BonMot

In this Altconf talk, we’ll learn about BonMot, an attributed string generation library for iOS. BonMot abstracts away all the weirdness and inconsistencies of NSAttributedString, providing a readable, usable, composable library that makes even the most complex typography a breeze.

I’ll be showing some ways that BonMot can help developers to step up their typography game and more closely honor designers’ work in their apps.


Introduction (0:00)

My name is Zev, and I work at Raizlabs in Boston, where I’m an iOS developer and design groupie. I want to tell you about BonMot, which is a library I wrote for making beautiful typography on iOS.

It all started when I was working on an app for Undercover Tourist, which is a Frog-themed app for amusement park enthusiasts. Our designer wanted to use beautiful print-inspired typography. On iOS, this means using NSAttributedString, which is great because it’s the result of years of research that have shown coupling between data and presentation to be ideal.

But working with NSAttributedString can be pretty frustrating. It’s not a fantastic expressive API.

BonMot (0:43)

Let’s say I wanted to write “Lightning Talk” in tightly spaced, large, white, centered text:

let label = UILabel()

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 0.7
paragraphStyle.alignment = .Center

let attributes = [
    NSFontAttributeName: UIFont.systemFontOfSize(150),
    NSForegroundColorAttributeName: UIColor.whiteColor(),
    NSParagraphStyleAttributeName: paragraphStyle,
]

label.attributedText = NSAttributedString(
    string: "Lightning talk",
    attributes: attributes)

With NSAttributedString, you have to:

  • Make a paragraphStyle.
  • Remember to make it mutable.
  • Set the lineHeightMultiple and alignment on the paragraphStyle, not the AttributedString directly, because I don’t know why
  • Pass in a bunch of keys. You have to get everything right in the dictionary.

It’s not the best. Using BonMot for the same task, you make a BONChain, which is the base unit of composition in BonMot. Then you set all of these properties in a clear, consistent way:

let label = UILabel()
label.attributedText =
    BONChain()
        .lineHeightMultiple(0.7)
        .alignment(.Center)
        .font(.systemFontOfSize(150))
        .color(.whiteColor())
        .string("Lightning Talk")
        .attributedString

You don’t have to worry about what’s going on under the hood. It takes care of paragraph styles, font descriptors, font stuff, text attachments, all of the things going on under the hood. Then you just ask for its AttributedString.

On slide 10, we have a slightly more advanced example. We’ve got the titles of some books on the left and their years of publication on the right:

let baseChain =
    BONChain()
        .color(.whiteColor())
        .font(.systemFontOfSize(50))

let titleChain = baseChain
    .alignment(.Left)

let yearChain = baseChain
    .alignment(.Right)

We can use a bit of inheritance where we make a baseChain, which has the color and font that’s shared between these labels. Then we make a titleChain with a left alignment and a yearChain with a right alignment. They inherit a lightweight version of CSS inheritance.

Text Alignment (1:31)

Then we can do interesting things like, if our designer says, “Hey, I really want those dates to line up in columns. We need to use monospace numbers.” Well, that’s easy:

let baseChain =
    BONChain()
        .color(.whiteColor())
        .font(.systemFontOfSize(50))

let titleChain = baseChain
    .alignment(.Left)

let yearChain = baseChain
    .figureSpacing(.Tabular)
    .alignment(.Right)

We can add figureSpacing(.tabular) to the year chains, and you can on slide 11 that doesn’t affect the books on the left that have numbers in them.

Similarly, if they want to track out the text on the left on the titles, we can do it without affecting the yearChain:

let baseChain =
    BONChain()
        .color(.whiteColor())
        .font(.systemFontOfSize(50))

let titleChain = baseChain
    .alignment(.Left)
    .pointTracking(22)

let yearChain = baseChain
    .figureSpacing(.Tabular)
    .alignment(.Right)

You can see the results of using pointTracking on slide 12.

By the way, when it comes to kerning versus tracking, this is a subtle difference that Apple gets wrong in their documentation. Kerning is the individual spacing between letter pairs, and this is something that’s added to the font by the font creator. Tracking is the overall spacing; you take text, and you spread it out or tighten it. The logical conclusion of all this is that improper kerning is known as “keming”.

Image & Text Alignment (2:45)

BonMot can make it really easy to work with images inline with strings. That is something that was added to iOS in iOS 7 with NSTextAttachment, but you don’t have to worry about how it works.

Using BonMot, you just pass in an image, and it will float with the text, which is difficult if not impossible with Auto Layout. You could also align labels and other text elements by capHeight and xHeight, something that you can’t do with Auto Layout out of the box. You can see two examples on slide 15 and 16.

Designers love to use special characters, all sorts of fancy Unicode characters for making beautiful typography. These characters don’t render very well in the monospace editor fonts in Xcode or other code editors. On slide 17, we’ve got a narrow, no-break space, and two hair spaces. If you were writing a unit test against this string, and you got something wrong, it would say, “These strings are not equal.” But you’d look and you wouldn’t be able to see the difference because these characters look identical.

BonMot can spit out a human readable testing string of this string with all the special characters replaced with human readable names. For example:

"{image108x108}{narrowNoBreakSpace}
AltConf June 13{hairSpace}{enDash}
{hairSpace}16"

Then it gives you a utility to test your strings against this template string in a unit test, and it’ll tell you where it’s wrong if it doesn’t match up.

Localization (3:56)

Finally, I want to talk about localization. Here we have a string:

let string = "I went to AltConf, and it was
<emphasis>not</emphasis> too cold!"

I want to emphasize the word “not,” but in French, the word “not” splits into two words. In other languages, you might have word order issues, or you might have a homonym that didn’t exist in English. If you were searching this string for a particular substring and replacing the attributes in its range, you might run into problems. BonMot makes this really easy:

let string = "I went to AltConf, and it was
<emphasis>not</emphasis> too cold!"

let chain = BONChain()
    .string(string)
    .font(.systemFontOfSize(50))
    .color(.whiteColor())
    .tagStyles([
        "emphasis": BONChain()
            .font(.boldSystemFontOfSize(50))
            .color(myBlue)
        ])

You can add the styling tag. In this example, I’ve added the <emphasis> tag around “not.” You can call the tags whatever you want, and then you make a BONChain for the base properties of the string, all the white text. Then you add a tagStyles method with the "emphasis" tag mapped onto a different font and a different color. That means that when your localizer adds these tags to a string in another language, it just gets applied automatically:

"Je suis allé à AltConf, et il <emphasis>n’</
emphasis>était <emphasis>pas</emphasis> trop
froid!"

And that’s all you have to do.

Conclusion (4:49)

Go check out the library at git.io/bonmot. I can’t wait to see the beautiful typography you create with it. :)

Resources


Zev Eisenberg

Zev Eisenberg

Zev is the son of two clowns who ran away to join the circuits. He does iOS development at Raizlabs in Boston, where he and his wife juggle and do acrobatics on weekends.