We recently launched a re-architected iOS app , with a faster experience and a new look and feel. Just as the architectural changes to the app were far-reaching, the visual design changes were equally ambitious. The new system standardizes typography, object placement and provides intelligent scaling so that a single design could be used across all iOS devices. To create this system, design and engineering worked hand in hand for several months. Our solution came in the form of our custom label class, “boints” and layout columns.
PITextNode: Managing fonts since 2016
The new Pinterest Design Standards provide a set of defaults to accelerate design. Fonts are limited to five “text” and “display” point sizes (display is used for headers whereas text is used for content). Designers specify these point sizes with names like “medium text” or “extra large display.” To keep everyone speaking the same language, we created an ASTextNode subclass, PITextNode , with convenience initializers with names like mediumTextNode or extraLargeDisplayTextNode . An engineer would have to work extra hard to make a text node that wasn’t one of the accepted fonts (and what engineer likes to work hard?). On the other hand, engineers can also push back on design if a mock is using a rogue font size.
This may come as a shock, but some of our designers are pretty big font nerds. Not only did they specify font sizes, word spacing, tracking and line spacing, they also requested a specific scaling for CJK (Chinese, Japanese and Korean) languages. Our default font, Helvetica Neue, appears too large when mixed with the default iOS CJK fonts of the same point size. PITextNode needed to scale CJK fonts to match the line height of Helvetica Neue at the same point size. Luckily, since we built on top of ASTextNode , we had access to the label’s NSTextStorage that’s used to render the text. NSTextStorage scans ASTextNode ’s internal NSAttributedString replacing all characters that do not exist in Helvetica Neue with their default iOS font. We iterated over all the fonts in the NSTextStorage and scaled the ones that weren’t Helvetica Neue (this even allowed us to shrink emojis so they didn’t cause line spacing issues).
Figure 1: Point sizes for text in Helvetica Neue fonts vs text in a Japanese font
PITextNode also handles scaling font sizes as the application window sizes changes. For example, a medium text font has a point size of 12 on smaller phones (e.g. iPhone 4 or iPhone 5), 14pt on iPhone 6 or iPhone 6+ and 16pt on tablets.
Figure 2: How fonts are defined in Pinterest Design Standards
Updating font sizes based on window size begins to hint at the true power of the new design system. As window size changes, object position and relative spacing also change. This is accomplished by two key features: what we call “boints” (points + BRIO [the internal design code name] = boints) and columns.
Boints: One unit to rule them all
The new design system describes the spacing between all objects in boints. A boint is a device independent number of points. As the device’s window size increases, so does the number of points in a boint. We distinguish between horizontal and vertical since objects begin to look too far apart vertically if we use the same horizontal spacing value. Here’s a quick rundown of vertical boints (voints) and horizontal boints (hoints) on different devices (I’m just kidding about that voints/hoints thing):
All design mocks are created with spacing in boints. If two objects are horizontally spaced by 2 boints they’ll appear 6 points apart on an iPhone 4 and 16 points apart on an iPad.
Layout columns: You are here
Layout columns provide locations on the canvas to place objects and determine sizing for Pinterest grids, such as home feed. The columns are computed dynamically based on well defined Design System rules:
- The number of columns is 12 for all window sizes
- The spaces between columns, called gutters, are 4 boints wide
- The minimum left and right margins of the screen are 4 boints
- Column widths must be integers
With these knowns we can solve for column width:
Solving this for a screen width of 320 gives us a column width of 9.333pts. Since we want integer columns, we take the slop (.3333 * column count) and split it between the left and right columns. Here’s what the columns look like on an iPhone 6 and an iPhone 4:
Once we have those prerequisites out of the way, we can make this statement about object placement in our design system:
- All objects are left-aligned to the start of a column* or
- All objects are right-aligned to the end of a column or
- An object is placed an integer number of boints away from an object that is aligned to a layout column
* All text aligned to a column has a soft indent of one boint.
If there’s one thing that engineers can get behind, it’s a bunch of explicit rules. Our product design team delivered a 75 page document detailing just that. From the spec, we created pluggable UI components like PITextNode . We also invested time in creating components to help make sure laying out on the BRIO grid was quick and easy.
Custom layout spec
We re-architected our iOS app on top of AsyncDisplayKit. To handle layouts, we chose AsyncDisplayKit’s newly added box model layout system. The box model relies heavily on layout specs. A layout spec describes how its children will lay out and computes their size and position. There are several built in layout specs in AsyncDisplayKit — including ASStackLayoutSpec and ASInsetLayoutSpec — but in order to easily display objects on our columns we had to create a custom layout spec.
Children of our custom spec explicitly state which column they are to be positioned on. By default, it’s assumed the object will be left aligned (right aligned is also supported). Optionally the child could explicitly declare its column span and vertical alignment. Here’s what this looks like in code:
PIColumnAlignedLayoutSpec *columnSpec = [[PIColumnAlignedLayoutSpec alloc] init]; self.textNode0.columnOffset = 0; self.textNode1.columnOffset = 3; self.textNode2.columnOffset = 8; self.textNode2.columnHorizontalAlignment = PIColumnHorizontalAlignmentRight; columnSpec.children = @[self.textNode1, self.textNode0, self.textNode2];
That was easy, right? Below are two examples of the column aligned spec in the Pinterest app (remember that text gets a soft indent of one boint off of a column):
So far the new system sounds like a cross disciplinary utopia, where designers and engineers are working in peace and harmony, sharing boba tea and working together to fight evil. But like any project, there were bumps along the way, and we engineered our way out of them. One example was the decision to use Sketch to design all mocks. There are many things about Sketch I love, but I don’t love its text bounding boxes (in fact, this will mostly be a rehash of an internal document called “Why I hate Sketch Bounding Boxes by Ricky Cancro”). First let’s take a look at the bounding box for text from iOS:
Notice there’s plenty of room below the baseline for descenders. Here, on the other hand, is Sketch’s version:
The bounding box is 6pts larger on iOS than in Sketch. This is a problem because our design team uses Sketch to lay out screens. In Sketch, something positioned 8 points (2 boints) below this text will appear an extra 6pts lower on iOS, so we had to figure out a way to get PITextNode to automatically handle these adjustments.
The first attempt to solve this involved looking at our ten font sizes and finding the insets to go from an iOS bounding box to a Sketch bounding box. Once we had these values, a mechanism was created so that every PITextNode was placed inside an ASInsetLayoutSpec with the appropriate insets before being added to a layout spec. This seemed to work great, until it didn’t.
Imagine you have an image and text in a horizontal stack. Of the two nodes, the text node is taller than the image. You’d like to center the image next to the text node like this:
The problem is the text node is wrapped in an inset spec with a negative bottom inset. While this makes vertical spacing work as desired, it also means the stack isn’t actually as tall as the gray bounding box. The stack spec is actually computed to be as tall as the outlined box below:
As you can see, the bear is centered within that outlined box, but when we remove all background coloring we get a layout where the bear looks anything but centered:
The problem with the first solution was that it affected the computed size of the layout spec. To fix this we needed to make layout specs aware of an inset on a node from which measurements are made, but which don’t affect the overall size of the computed layout. This idea is similar to UIKit’s layout guides.
We implemented another custom layout spec, PIStackLayoutSpec , a subclass of ASStackLayoutSpec that takes a second pass through the sublayouts and moves their position based on the layoutable object’s layoutGuideInsets . When PITextNodes are created, they’re given the proper layout guides to convert between Sketch an iOS bounding boxes.
Here’s the above example when rendered using layoutGuideInsets instead of ASInsetLayoutSpec :
For a more complicated example, here are two text nodes vertically stacked and placed in a horizontal stack with a centered aligned image:
The gray bounding box is semi-transparent, so you can see the smaller text node is positioned inside the large text node’s bounding box. The top most horizontal stack spec will inherit top and bottom layout guides from the large and small text nodes, respectively. This allows us to nest text nodes in stacks, and continue to propagate the proper layout guides.
The promise of the new system is one design for all devices — meaning design for the iPhone will also look good on the iPad thanks to boints and columns. While we were thrown some curve balls during the design and implementation of BRIO (looking at you, iPad Pro), the system has overall worked extremely well and set the stage for rigorous design standards at Pinterest.
Acknowledgements: This deep dive into boints, columns, leading, and kerning couldn’t have happened without: Long Cheng, Jay Marsh, Tom Watson, Garrett Moon, Scott Goodson and anyone who listened to me complain about text bounding boxes.