Exploring SwiftUI: Color System
Book

Exploring Freelancing

Navigate freelancing as a developer; find clients, manage contracts, ensure timely payment, and learn from experiences!

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!

Book

Exploring Freelancing

Navigate freelancing as a developer; find clients, manage contracts, ensure timely payment, and learn from experiences!

Written by

Rudrank Riyam

Hi, my name is Rudrank. I create apps for Apple Platforms while listening to music all day and night. Author of "Exploring MusicKit". Apple WWDC 2019 scholarship winner.