Managing Color Schemes in SwiftUI

Deciding how to manage multiple color schemes in an iOS app can be tricky. This is partially due to the fact that there are so many different ways to go about it. Here we will outline one possible way that we use frequently.

Creating Colors

First, in order to create a color scheme, we need to create colors. The default way to do this in iOS is usually using RGB values. However, we like using Hex instead. As such, we use the following extension¹:

extension UIColor {
    convenience init(hex: String) {
        let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
        var int: UInt64 = 0
        Scanner(string: hex).scanHexInt64(&int)
        let a, r, g, b: UInt64
        switch hex.count {
        case 3: // RGB (12-bit)
            (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
        case 6: // RGB (24-bit)
            (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
        case 8: // ARGB (32-bit)
            (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
        default:
            (a, r, g, b) = (1, 1, 1, 0)
        }
        
        self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255, alpha: Double(a) / 255)
    }
}

This allows us to create colors like so:

UIColor(hex: "FFFFFF") // white color

Now, if you are familiar with SwiftUI, you may be wondering why we are using UIColor instead of Color. The reason for this is dark mode support. UIColor provides an initializer that gives us access to the knowledge of whether or not the phone is currently in dark mode. Color from SwiftUI does not have this. As such, we need to create UIColors and then convert them to Colors.

First, let's create a custom function for UIColor that will allow us to provide two input colors: one for light and one for dark mode.

extension UIColor {
    static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
        guard #available(iOS 13.0, *) else { return light } // feel free to omit this if you are targeting only later iOS versions
        return UIColor { $0.userInterfaceStyle == .dark ? dark : light }
    }
}

Now, let's create a function for Color that wires this all together:

extension UIColor {
    // helper to convert from UIColor -> Color
    func toColor() -> Color {
        return Color(uiColor: self)
    }
}
extension Color {
    static func dynamic(light: String, dark: String) -> Color {
        let l = UIColor(hex: light)
        let d = UIColor(hex: dark)
        return UIColor.dynamicColor(light: l, dark: d).toColor()
    }
}

With that all set up we can now create colors that change for dark mode as follows:

// white in light mode and black in dark mode
Color.dynamic(light: "FFFFFF", dark: "000000")

Creating the Color Scheme

Depending on your application, you will need different colors in your color scheme. Some apps will need more background colors, other more accent colors and so on. For the sake of simplicity, we will create a very simple example here.

struct MyColorScheme {
    let background: Color
    let accent: Color
    let text: Color
}

Now we can use this class to store our colors and make the accessible throughout our application.

Color Scheme Types

If you want to allow the user to change between multiple color schemes, you will need a mechanism for storing and differentiating between them. For this we will use an Enum.

enum MyColorSchemeType {
    case main
    case alternative

    private static let mainCs: MyColorScheme = MyColorScheme(
        background: Color.dynamic(light: "FFFFFF", dark: "000000"),
        accent: Color.dynamic(light: "FF0000", dark: "0000FF"),
        text: Color.dynamic(light: "000000", dark: "FFFFFF")
    )

    private static let altCs: MyColorScheme = MyColorScheme(
        background: Color.dynamic(light: "FFFFFF", dark: "000000"),
        accent: Color.dynamic(light: "00FF00", dark: "FF00FF"),
        text: Color.dynamic(light: "000000", dark: "FFFFFF")
    )

    var colorScheme: MyColorScheme {
        switch self {
        case .main:
          return MyColorSchemeType.mainCs
        case .alternative:
          return MyColorSchemeType.altCs
        }
    }
}

Here we have created an enumeration that keeps track of our different color schemes. Further, it will allow us to have a current color scheme that we can return with the computed var colorScheme. This will make more sense as we proceed.

Using in SwiftUI

Finally, we can look at how this is actually used in SwiftUI. First, let's create a class that we can use to track our current color scheme.

final class ColorSchemeState: ObservableObject {
    @Published private var currentColorSchemeType = MyColorSchemeType.main // default to main
    var current: MyColorScheme {
        return currentColorSchemeType.colorScheme // return the actual color scheme based on the current type
    }

    func updateColorScheme(to colorSchemeType: MyColorSchemeType) {
        self.currentColorSchemeType = colorSchemeType
    }
}

Then we can use this in a random SwiftUI view as follows:

// This would probably be a view near the very top of your app's view hierarchy
struct OuterView: View {
    @StateObject var colorSchemeState = ColorSchemeState()
    var body: some View {
        InnerView()
            .environmentObject(colorSchemeState)
    }
}

struct InnerView: View {
    @EnvironmentObject var colorScheme: ColorSchemeState
    var body: some View {
        VStack {
            Text("Hi!").foregroundColor(colorScheme.current.text)
            Button {
                colorScheme.updateColorScheme(to: .alternative)
            } label: {
                Text("Change").foregroundColor(colorScheme.current.accent)
            }
        }.background(colorScheme.current.background)
    }
}

Here we have created an instance of the ColorSchemeState as a state object and passed it down as an EnvironmentObject. From there, we created a button that allows a user to change the color scheme from the main to the alternative.

Remember, you can hit Command + Shift + A on your keyboard to change the iOS simulator from light to dark mode. Trying that out you will be able to see the colors change as we configured them above.

Conclusion

In this post, we went through and showed our favorite way to structure color schemes and pass their state through a SwiftUI application. Hopefully this gives you a good starting point to make this happen in your own application.

Footnotes

[1] Note, we didn't write this extension, but rather found it on stack overflow or something a while back. We can't find the exact post or we would link it here. Regardless, you can find many similar examples by searching UIColor hex on google.