Exploring MusicKit and Apple Music API
Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!
While working on the next update for Gradient Game, I realized that the colors are all over the place, and inconsistencies are creeping in. Someplace it is UIColor.systemGray5
, someplace it is Color.systemGray
.
I created a folder for Design System to overcome this problem and have consistency across the app for a smoother future update cycle. This is the part of the series that talks about the colors.
I’ve already written about using Custom Colors in SwiftUI. My earlier approach was simple. Create new color sets, add both light and dark mode variants as hex values, and name it accordingly.
Extension on Color
Then, create an extension on Color
and add static constants on it. Pretty simple. This was my implementation back in 2019:
extension Color {
/// Main branding colors
static let brand = Color("brand")
static let mainColor = Color("mainColor")
static let notMainColor = Color("notMainColor")
static let customBackgroundColor = Color("customBackgroundColor")
}
Apart from my horrible naming conventions, this works fine. To use it in SwiftUI:
struct TitleView: View {
var body: some View {
Text("GRADIENTS GAME")
.customTitleText(size: 30)
.foregroundColor(.mainColor) // <- Custom Color
}
}
To make it better, I searched for other options. The idea of having a color system, according to me, is to have a single source of truth to define and easily update the colors in the future.
And you only use the colors defined in that one place and nothing else, so everyone in the team is on the same page.
I stumbled upon this article by Ram Shandilya on Building a Design System for iOS - Part 3 - Colors. One idea that stood out was to define our own colors and not depend on the system initializer. This article is inclined towards UIKit, so I’m penning down my experience of using this idea in your SwiftUI views.
Xcode’s Asset Catalog
As mentioned earlier, having a visual look at the colors, both in light and dark mode, is a great way to create color sets in one place. The best place for this is in Xcode’s Asset Catalog. Designers I’ve worked with usually give the colors in a hex value, so you can add them in here.
For naming, I typically use camel casing, so that I don’t have to add the string while defining the enum as well. We’ll look at this in the next section.
Enum for Color Types
To start, we define an enum ColorType
that has various cases:
enum ColorType: String, CaseIterable {
/// Branding colors
case branding
case secondaryBranding
/// Text colors
case headline
case subheadline
/// Button colors
case primaryAction
case secondaryAction
/// Background colors
case background
case systemBackground
case secondaryBackground
/// Progress bar color for the portion completed
case progress
case divider
var name: String {
self.rawValue
}
}
They’re named according to their intent, where branding
and secondaryBranding
are the brand colors, headline
and subheadline
are the colors for the texts, the background
is the primary background color in every view, etc.
As we defined the colors in the Asset Catalog with camel casing, we don’t have to write it again for the cases in the enum.
Extensions on View and Shape
In the app, there are fours ways the colors are used:
- foreground color for the label or images
- background for a view
- fill in a shape
- stroke in a shape
To ensure I use only ColorType
everywhere, I defined two extensions on View
and two on Shape
:
extension View {
func color(_ color: ColorType) -> some View {
self.foregroundColor(Color(color.name))
}
func background(_ color: ColorType) -> some View {
self.background(Color(color.name))
}
}
extension Shape {
func fill(_ color: ColorType) -> some View {
self.fill(Color(color.name))
}
func stroke(_ color: ColorType) -> some View {
self.stroke(Color(color.name))
}
}
It works the same way as the previous one:
Text(item.name)
.color(isSelected ? .headline : .subheadline)
But, now, I know that I’m only using the cases defined in the enum ColorType
. If I try to use any other class like .gray
instead, it’ll give an error.
You’ll also see a case primaryAction
. It isn’t #FFFFFF but a slightly tinted variation. It is used as the foreground color of the text in the primary action buttons.
Also, make sure your custom names don’t coincide with the system names. It’ll work fine, but the previews will crash. I had first-hand experience, so I replaced primary
with branding
and secondary
with secondaryBranding
respectively.
Testing
To make sure that the color exists in the Asset Catalog, you’ll write a unit test. I still haven’t figured out a way to test SwiftUI’s Color
and am eager if you know how to do it. For now, I’m using the test method mentioned by Ram Shandilya in their post.
You’ll know if you’re using a case that doesn’t have a counterpart in the catalog when running them:
class ColorTests: XCTestCase {
func testColorsExistInAssetCatalog() {
for type in ColorType.allCases {
let color = UIColor(named: type.name)
XCTAssertNotNil(color, "Asset catalog is missing an entry for \(type.name)")
}
}
}
Conclusion
I know this seems to be overcomplicated, but it has worked well for me, and I’ll update the post if I find a better way to create the color system.
If you have a better approach, please tag @rudrankriyam on Twitter! I love constructive feedback and appreciate constructive criticism.
Thanks for reading, and I hope you’re enjoying it!
Exploring MusicKit and Apple Music API
Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!