May 17, 2020 • 4 min read
Building a Design System for iOS - Part 3 - Colors
This is part 3 of the Building a Design System for iOS series. Head over to the previous posts on the introduction to design system & typography in case you missed it.
Colors, if done correctly, can be easy to maintain. Otherwise, it's super easy to misuse. You'll know your app's true colors (pun intended) when you're being asked to change the branding color values.
Let's start with a couple of guidelines to succeed -
- Single source of truth - one place to add/update colors
- Semantic naming - The names shouldn't change even when the app undergoes a rebranding.
- No custom colors in XIBs and Storyboards.
- Lose custom helper initializers - those initializers for hex or hsb values
- We could even go all the way and disable calling any
UIColor
s outside of our defined colors.
With that established, let's get started.
Single source of truth
It's important to maintain a single source of truth for colors as your application evolves. If your app undergoes a redesign, there should be only one place to update the colors.
Xcode's Asset Catalog is a great place to manage colors. With the introduction of Dark Mode in iOS 13, each color can have a dark mode counterpart. The colors added in the Asset Catalog are accessible both in code and interface builder files.
Semantic naming
Naming your colors based on intent rather than how it looks can help avoid a lot of pain points. For example, assume we're building an app for Hacker News, instead of naming Orange
, we can name it Primary
. That way, we never have to change anything in code if some day in the future Hacker News rebrands to a purple color. All we have to do is just swap the color in the Asset Catalog.
Here's an example of color names that I chose for our sample app -
Generating the colors in code is pretty straightforward. You can create static constants in UIColor
much like default colors like .red
, .green
.
extension UIColor {
static let primary = UIColor(named: "Primary")!
}
Although this is neat, and it gives me UIColor.primary
, I prefer not to have static constants in the app. We're better off creating an Enum
so the color can be initialized as and when needed.
enum Color: String, CaseIterable {
//Base Colors
case primary = "Primary"
case error = "Error"
// Text Colors
case textPrimary = "Text Primary"
case textSecondary = "Text Secondary"
//Background Colors
case backgroundPrimary = "Background Primary"
case backgroundSecondary = "Background Secondary"
//Action Text Colors
case actionTextPrimary = "Action Text Primary"
case actionTextOnColor = "Action Text On Color"
}
And now we can define a convenience initializer on UIColor
to use our Color
type.
extension UIColor {
convenience init(color: Color) {
self.init(named: color.rawValue)!
}
}
The init(named:)
is a returns and optional UIColor
and hence the implicitly unwrapped optional. I'm also goning to write a unit test to back this, in case the initializer fails.
class ColorTests: XCTestCase {
func testColorsExistInAssetCatalog() {
for color in Color.allCases {
let uiColor = UIColor(named: color.assetName)
XCTAssertNotNil(uiColor, "Asset catalog is missing an entry for \(color.rawValue)")
}
}
}
This way, if you have a CI/CD setup, you're aware when the initializer fails and crashes.
Our new color usage will look like this -
UIColor(color: .textPrimary)
It's not as good as UIColor.red
, but this way we're avoiding static constants and Xcode's autocomplete can also help you identify what colors the app uses. Nifty!
A Little HouseKeeping
We're ready to use our new color system. If you have an existing app where colors are all around the place, you may need help in identifying colors, especially in storyboards. Best way is to do a grep
search from the terminal. For e.g. you can do a grep -r "name=\"Dark Blue\"" *.storyboard *.xib .
to find all instances of usages of Dark Blue
in XIBs & Storyboards.
There are also tools like ibcolortool, made by nshipster, which can list all the colors in XIBs & Storyboards.
ibcolortool list **/*.{xib,storyboard} --format rgbhex
A quick run of the tool gave me all the usage of UIColor
. It's super handy.
Add restrictions
In order for the color system to stay clean over time, we need to put some restrictions in place. Linters help a lot in this case. I'd highly recommend linters to consider any usages of UIColor
initializers to produce warnings. This way, your team is forced to use the colors from the new system. There are tools like xiblint made by folks at Lyft, which is a tool for linting storyboard and xib files. You can add rules to XIBs such as named_colors
which allows only named colors and not any custom colors.
That's it. We've come a long way in defining our design system. Next up Animations
This is part three in a series of blog posts highlighting my experience in building different components of a design system for iOS. In case you want to jump to other parts -