Building a native design system for iOS

By: Kevin Beaulieu

Thumbtack Engineering
Thumbtack Engineering
6 min readFeb 28, 2020

--

Photo by William Iven on Unsplash

What is a “native design system”?

“Native design system” can be broken up into two parts: “native” and “design system.” Starting with the latter, a design system is a collection of reusable components, tokens, and other design standards that can be used by both designers, to guide their work, and software engineers, to speed up the process of implementing UIs. A native design system is specifically a design system built for native iOS & Android apps. In the case of Thumbtack’s design system, Thumbprint, our native design system shares much of its content with its web counterpart, but there are native-specific concerns factored in as well.

Why now?

Thumbtack has had a web design system for awhile now, but as the company has shifted its focus towards our native apps, it felt incomplete for our design system to only include components for our website. A design system provides consistency throughout the product, which is an important part of strengthening Thumbtack’s brand identity. By taking a principled approach to defining a common set of design standards, we also minimize the problem of engineers building the same (or nearly the same) thing many times over.

By putting an emphasis on accessibility and detail, we end up improving product quality by adopting a design system as well. Building a polished, accessible app can be extremely difficult if every page uses its own custom components, since the work to make high quality views must be duplicated across every page. By standardizing around a single set of components, we can put in the work once and reap the benefits across the entire app.

Architecting a new component

In starting our design system on iOS, we’ve used a few principles to guide our implementation:

Parity should be maintained across platforms where possible.

While this post focuses on the iOS implementation of Thumbprint, our design system is supported on Android and web as well. As such, it is useful to maintain consistency between platforms. There are many dimensions across which parity can be considered, but we focus primarily on two:

Name parity: Platforms often refer to components differently. For instance, what iOS engineers call a text field, web engineers call an input. When everyone is using different names to talk about the same component, discussion can quickly get confusing. Therefore when building new components, we standardize on a single name to use across all platforms.

Design parity: Consistency in the appearance of components is likewise important for ensuring that users experience the Thumbtack brand in a consistent, recognizable way regardless of the device they are using. There are some exceptions to this guideline, such as when different platforms have different design patterns themselves, but for the most part we try to maintain a consistent experience across our iOS, Android, and web products.

Standard usage should be easy. Customization should be possible.

When writing code that will be consumed by other engineers, there is a balance between a strongly opinionated architecture, which maximizes consistency, and a highly flexible architecture, which ensures as many people can use it as possible.

To decide where to land on this spectrum, consider that a design system has two primary goals:

  1. Increase product quality
  2. Increase developer productivity

If an engineer is implementing a design that fits squarely within the bounds of the design system, we should make that as easy for them as possible. If a design is breaking some of the design system guidelines, we should make that harder, to serve as a cautionary reminder that what the engineer is trying to do is not standard. That said, if the design is only slightly beyond the guidelines, we shouldn’t require the engineer to build everything from scratch — the amount of friction introduced should be proportional to the rules being bent/broken.

As an example of how we optimized for this, we can take a look at our Button component. Buttons in Thumbprint can have one of five themes: primary, secondary, tertiary, caution, or solid. The button theme defines things like the button’s title color, its background and border colors, if any, and whether it supports a loading state. These five themes serve 99% of button needs within our apps, but there’s always that 1% of pages that need a button which doesn’t quite fit into one of these themes. To enable engineers to create such custom buttons, we implemented button themes as a struct with a public constructor.

public struct Theme {
public let titleColor: UIColor?
public let activeTitleColor: UIColor?
public let disabledTitleColor: UIColor?
public let backgroundColor: UIColor?
...
}

Defining the five standard themes was then just a matter of creating constants with specific constructor arguments:

public struct Theme {
...
public static let primary = Theme(titleColor: Color.white, ...)
public static let secondary = Theme(titleColor: Color.black, ...)
...
}

So for the 99% of cases where a standard button theme is sufficient, engineers can construct a button like Button(theme: .secondary). Then for one-offs, all they need to do is call the Theme constructor directly with whatever properties they want their custom theme to have, and then pass that into the Button constructor instead. If a custom button needs to deviate even further from the design system, such that customizing the Theme properties is insufficient, then the engineer is forced to fall back on using vanilla UIKit to implement their custom button from scratch. While this requires additional work, the friction serves as a reminder to the engineer & designer that they are not following the guidelines of the design system. We have found this pattern of defining components' properties as Swift structs to work quite well.

Using Thumbprint should feel familiar to engineers who have experience with UIKit.

When building a view, iOS engineers at Thumbtack have two main options: UIKit and our internal design system, Thumbprint. Since both play important roles in our codebase, migrating from one to the other should not require a huge shift in mindset or code architecture. In this spirit, Thumbprint components subclass UIKit controls where possible and preserve the same default values for properties that exist in both UIKit and Thumbprint where reasonable. This may seem like a no-brainer, but there are some cases where this decision has come up in reasoning about how a new component should be implemented. Our Label component is one example: Thumbprint.Label is a subclass of UILabel which automatically applies our font and color to the label. When building the component, there was some debate about whether we should additionally set the default value of numberOfLines to 0. In Thumbtack’s native apps, we typically prefer wrapping text content to truncating it, and as such find ourselves setting label.numberOfLines = 0 on just about every new label. SO it would be reasonable to just set this as the default in our internal Label component. However this would result in a substantial cognitive load when context switching between using UILabels and Thumbprint Labels. Engineers would constantly have to think about which they were working with, and remember what the default value is for each. Instead, we settled on preserving UIKit’s default.

What’s next?

Our native design system is only about a year old, and as such we are still iterating on it fairly quickly. A few goals in mind for the future are to standardize more tokens and component properties across platforms. For instance, we already define color presets as JSON in a shared repo, which then gets exported as a CocoaPod for iOS, and analogous modules for Android and web. We could extend this further by defining component themes in a similar format , which would then generate the Swift code for the Theme struct we saw above. As our design system matures, we also hope to open-source it, as we have already done for our web design system. In addition to building out the implementation itself, we also hope to improve our documentation of the design system on thumbprint.design. The website was originally built to document the web portion of our design system, but as we expand our iOS design system we plan to include more documentation of that as well.

Originally published at https://engineering.thumbtack.com on February 28, 2020.

--

--

Thumbtack Engineering
Thumbtack Engineering

We're the builders behind Thumbtack - a technology company helping millions of people confidently care for their homes.