Backwards compatibility for iOS 13 system colors

June 9, 2019

Update (6/13/2019): The original version of this post featured the wrong colors in the ColorCompatibility enum at the end of the post. This has now been fixed.

Update (9/21/2019): The original version of this post included systemBrown, a color which was removed in the Xcode 11 GM. It also contained unnecessary availability checks in the code at the bottom (system colors like red, blue etc have been available since iOS 7). The code has been updated, and the tables in this post still list all system and element colors - even those which have been available for a while, since they're different in dark mode vs light mode. I also updated a reference to Mac Catalyst.

Update (10/5/2019): A previous version of this post included the wrong default colors in the ColorCompatibility code at the bottom of the post. These have been updated - the defaults are now the light mode colors which are being used in production in Trestle and CIFilter.io.

Update (10/5/2019): In order to better support bug reporting on ColorCompatibility, I've released it as a library. The code now lives in this GitHub repo. You can read more about it here.

Update (10/17/2019): A previous version of this post included the wrong color values in one of the example code blocks. This has now been fixed, but I'd recommend looking at the ColorCompatibility library if you're interested in using it in your own projects.

At WWDC 2019, Apple announced that Dark Mode would be supported on iOS 13. There are some significant changes to UIKit in order to support this - many of them are detailed in the talk Implementing Dark Mode on iOS which I'd highly recommend watching.

One of the changes that makes adopting Dark Mode so easy is the new system colors API from UIColor. On iOS 12 and older, you might have a label you want to make black, and it would work just fine - in fact, black was the default color for UILabels.

label.textColor = UIColor.black

But in Dark Mode, the background will also be black, which means the text won't be visible. In iOS 13+, it's better to use the new system color which will respect the user's color scheme preference:

label.textColor = UIColor.label

label is only one example: there are 24 new color scheme agnostic UIColors available in iOS 13+.

Colors are organized into two groups:

  1. Element Colors (e.g. label)
  2. Standard Colors (systemIndigo, systemGray3, etc).

iOS 13 has 23 new element colors (label etc) and one new standard color (systemIndigo). However, even system colors that have been around for a while (like systemRed) have become dynamic in iOS 13 - they might actually be different colors in dark mode vs light mode.

(Skip to the bottom if you'd like to see a list of the new colors.)

Compatibility

These new colors are all well and good, but most of us with existing apps will still be supporting devices with iOS 12 or lower, at least for a while. This means we'll probably be doing a lot of things like this, using Swift's #available syntax:

if #available(iOS 13, *) {
    label.textColor = .label
} else {
    label.textColor = .black
}

It's a workable solution, but it necessitates changing a lot of code - an if statement for every custom label or background color! For CIFilter.io, I wondered if there was a better way. What if, instead of the if #available, there was a way to abstract the color choice down one level, so we could do something like this?

label.textColor = ColorCompatibility.label

Generating system colors

I wrote a small app (with SwiftUI, no less!) which displays all the system colors in the current color scheme.

System colors (light mode)
Light mode
System colors (dark mode)
Dark mode

This app collects all the UIColor objects - once we have those, we can use their red/green/blue/alpha components to generate the implementation of ColorCompatibility that we want:

enum ColorCompatibility {
    static var label: UIColor {
        if #available(iOS 13, *) {
            return .label
        }
        return UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
    }
    static var secondaryLabel: UIColor {
        if #available(iOS 13, *) {
            return .secondaryLabel
        }
        return UIColor(red: 0.23529411764705882, green: 0.23529411764705882, blue: 0.2627450980392157, alpha: 0.6)
    }

    // ... 21 more definitions: full code in the link at the bottom
}

We can then use ColorCompatibility it to set any colors we need.

Note: we can't use @available for these checks, since it doesn't provide a way to check if the current environment is less than a specific app version.

Conclusion

This approach has some great advantages:

  1. This code will compile on iOS 13+, iOS 12 and earlier, and Catalyst ✅
  2. Since everything is a computed var, we never pre-store colors, which means that when the user switches color scheme, our app will automatically adapt as the trait collection changes 👍
  3. When we drop iOS 12 support, cleaning this up will be as simple as replacing every instance of ColorCompatibility with UIColor 🎉

Hopefully this makes your app's transition to dark mode easier!

Table of system colors

For those interested in iOS 13+ system colors but not wanting (or not able) to compile the sample app, here's a list of the system colors in light and dark mode, with their hex codes and RGBA values:

Light Mode

NameColorHex StringRGBA
label
#000000ffrgba(0.0, 0.0, 0.0, 1.0)
secondaryLabel
#3c3c4399rgba(60.0, 60.0, 67.0, 0.6)
tertiaryLabel
#3c3c434crgba(60.0, 60.0, 67.0, 0.3)
quaternaryLabel
#3c3c432drgba(60.0, 60.0, 67.0, 0.18)
systemFill
#78788033rgba(120.0, 120.0, 128.0, 0.2)
secondarySystemFill
#78788028rgba(120.0, 120.0, 128.0, 0.16)
tertiarySystemFill
#7676801ergba(118.0, 118.0, 128.0, 0.12)
quaternarySystemFill
#74748014rgba(116.0, 116.0, 128.0, 0.08)
placeholderText
#3c3c434crgba(60.0, 60.0, 67.0, 0.3)
systemBackground
#ffffffffrgba(255.0, 255.0, 255.0, 1.0)
secondarySystemBackground
#f2f2f7ffrgba(242.0, 242.0, 247.0, 1.0)
tertiarySystemBackground
#ffffffffrgba(255.0, 255.0, 255.0, 1.0)
systemGroupedBackground
#f2f2f7ffrgba(242.0, 242.0, 247.0, 1.0)
secondarySystemGroupedBackground
#ffffffffrgba(255.0, 255.0, 255.0, 1.0)
tertiarySystemGroupedBackground
#f2f2f7ffrgba(242.0, 242.0, 247.0, 1.0)
separator
#3c3c4349rgba(60.0, 60.0, 67.0, 0.29)
opaqueSeparator
#c6c6c8ffrgba(198.0, 198.0, 200.0, 1.0)
link
#007affffrgba(0.0, 122.0, 255.0, 1.0)
darkText
#000000ffrgba(0.0, 0.0, 0.0, 1.0)
lightText
#ffffff99rgba(255.0, 255.0, 255.0, 0.6)
systemBlue
#007affffrgba(0.0, 122.0, 255.0, 1.0)
systemGreen
#34c759ffrgba(52.0, 199.0, 89.0, 1.0)
systemIndigo
#5856d6ffrgba(88.0, 86.0, 214.0, 1.0)
systemOrange
#ff9500ffrgba(255.0, 149.0, 0.0, 1.0)
systemPink
#ff2d55ffrgba(255.0, 45.0, 85.0, 1.0)
systemPurple
#af52deffrgba(175.0, 82.0, 222.0, 1.0)
systemRed
#ff3b30ffrgba(255.0, 59.0, 48.0, 1.0)
systemTeal
#5ac8faffrgba(90.0, 200.0, 250.0, 1.0)
systemYellow
#ffcc00ffrgba(255.0, 204.0, 0.0, 1.0)
systemGray
#8e8e93ffrgba(142.0, 142.0, 147.0, 1.0)
systemGray2
#aeaeb2ffrgba(174.0, 174.0, 178.0, 1.0)
systemGray3
#c7c7ccffrgba(199.0, 199.0, 204.0, 1.0)
systemGray4
#d1d1d6ffrgba(209.0, 209.0, 214.0, 1.0)
systemGray5
#e5e5eaffrgba(229.0, 229.0, 234.0, 1.0)
systemGray6
#f2f2f7ffrgba(242.0, 242.0, 247.0, 1.0)

Dark Mode

NameColorHex StringRGBA
label
#ffffffffrgba(255.0, 255.0, 255.0, 1.0)
secondaryLabel
#ebebf599rgba(235.0, 235.0, 245.0, 0.6)
tertiaryLabel
#ebebf54crgba(235.0, 235.0, 245.0, 0.3)
quaternaryLabel
#ebebf52drgba(235.0, 235.0, 245.0, 0.18)
systemFill
#7878805brgba(120.0, 120.0, 128.0, 0.36)
secondarySystemFill
#78788051rgba(120.0, 120.0, 128.0, 0.32)
tertiarySystemFill
#7676803drgba(118.0, 118.0, 128.0, 0.24)
quaternarySystemFill
#7676802drgba(118.0, 118.0, 128.0, 0.18)
placeholderText
#ebebf54crgba(235.0, 235.0, 245.0, 0.3)
systemBackground
#000000ffrgba(0.0, 0.0, 0.0, 1.0)
secondarySystemBackground
#1c1c1effrgba(28.0, 28.0, 30.0, 1.0)
tertiarySystemBackground
#2c2c2effrgba(44.0, 44.0, 46.0, 1.0)
systemGroupedBackground
#000000ffrgba(0.0, 0.0, 0.0, 1.0)
secondarySystemGroupedBackground
#1c1c1effrgba(28.0, 28.0, 30.0, 1.0)
tertiarySystemGroupedBackground
#2c2c2effrgba(44.0, 44.0, 46.0, 1.0)
separator
#54545899rgba(84.0, 84.0, 88.0, 0.6)
opaqueSeparator
#38383affrgba(56.0, 56.0, 58.0, 1.0)
link
#0984ffffrgba(9.0, 132.0, 255.0, 1.0)
darkText
#000000ffrgba(0.0, 0.0, 0.0, 1.0)
lightText
#ffffff99rgba(255.0, 255.0, 255.0, 0.6)
systemBlue
#0a84ffffrgba(10.0, 132.0, 255.0, 1.0)
systemGreen
#30d158ffrgba(48.0, 209.0, 88.0, 1.0)
systemIndigo
#5e5ce6ffrgba(94.0, 92.0, 230.0, 1.0)
systemOrange
#ff9f0affrgba(255.0, 159.0, 10.0, 1.0)
systemPink
#ff375fffrgba(255.0, 55.0, 95.0, 1.0)
systemPurple
#bf5af2ffrgba(191.0, 90.0, 242.0, 1.0)
systemRed
#ff453affrgba(255.0, 69.0, 58.0, 1.0)
systemTeal
#64d2ffffrgba(100.0, 210.0, 255.0, 1.0)
systemYellow
#ffd60affrgba(255.0, 214.0, 10.0, 1.0)
systemGray
#8e8e93ffrgba(142.0, 142.0, 147.0, 1.0)
systemGray2
#636366ffrgba(99.0, 99.0, 102.0, 1.0)
systemGray3
#48484affrgba(72.0, 72.0, 74.0, 1.0)
systemGray4
#3a3a3cffrgba(58.0, 58.0, 60.0, 1.0)
systemGray5
#2c2c2effrgba(44.0, 44.0, 46.0, 1.0)
systemGray6
#1c1c1effrgba(28.0, 28.0, 30.0, 1.0)

ColorCompatibility full code

The full, generated implementation of ColorCompatibility, which is used by Trestle and CIFilter.io, is available here.