Black Friday 2024 deal: 30% off our Swift and SwiftUI books! Learn more ...Black Friday 2024 deal:30% off our Swift and SwiftUI books >>

Encode and decode SwiftUI color

SwiftUI Color type doesn't conform to Codable by default. If we need to save it to disk as part of a Codable type or on its own we need to define how it should be encoded ourselves.

For encoding purposes we can divide SwiftUI colors into 2 types: constant and dynamic colors. Constant colors don't change based on context, don't have different light and dark appearance and don't react to the environment. Dynamic colors are context-dependent and automatically adapt to appearance and other settings.

When we encode a color in SwiftUI we should take into account whether we are dealing with a constant or dynamic color.

# Encoding constant colors

Constant colors are usually created from Core Graphics color, from RGB or HSB components, or from constant UIKit and AppKit colors.

# Conversion to UIColor/NSColor

The easiest way to encode SwiftUI color is to convert it to UIColor or NSColor and leverage the fact that platform colors conform to NSSecureCoding.

If we are only dealing with Apple platforms and the encoded color will be decoded on one of those platforms, we can go with the described solution.

Here is an example of how to implement encoding and decoding for SwiftUI Color that will work on both iOS and macOS.

#if os(iOS)
typealias PlatformColor = UIColor
extension Color {
    init(platformColor: PlatformColor) {
        self.init(uiColor: platformColor)
    }
}
#elseif os(macOS)
typealias PlatformColor = NSColor
extension Color {
    init(platformColor: PlatformColor) {
        self.init(nsColor: platformColor)
    }
}
#endif

let color = Color(.sRGB, red: 0, green: 0, blue: 1, opacity: 1)

func encodeColor() throws -> Data {
    let platformColor = PlatformColor(color)
    return try NSKeyedArchiver.archivedData(
        withRootObject: platformColor,
        requiringSecureCoding: true
    )
}

func decodeColor(from data: Data) throws -> Color {
    guard let platformColor = try NSKeyedUnarchiver
            .unarchiveTopLevelObjectWithData(data) as? PlatformColor
        else {
            throw DecodingError.wrongType
        }
    return Color(platformColor: platformColor)
}

enum DecodingError: Error {
    case wrongType
}
Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Black Friday 2024 offer: 30% off!$35$25

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms
Black Friday 2024 offer: 30% off!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35$25

# Conversion to CGColor

If we have to encode a representation of the color that can be used on other platforms, for example on the web, we need to reach for CGColor and get the color components from there.

To get CGColor from a constant SwiftUI color, we can read its cgColor property. Note, that it's only available for constant colors and will return nil if we try to access it on a dynamic color.

let constantColor = Color(.sRGB, red: 1, green: 0, blue: 0.5, opacity: 1)
let cgColor1 = constantColor.cgColor // Optional CGColor

let dynamicColor = Color.blue
let cgColor2 = dynamicColor.cgColor // nil

Once we have the Core Graphics representation of the color, we can encode its color space and components. Depending on where and how the encoded color will be used, we need to define our own encoding logic.

We could define a Codable type that encapsulates a CGColor in the following way.

struct CodableColor: Codable {
    let cgColor: CGColor
    
    enum CodingKeys: String, CodingKey {
        case colorSpace
        case components
    }
    
    init(cgColor: CGColor) {
        self.cgColor = cgColor
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder
            .container(keyedBy: CodingKeys.self)
        let colorSpace = try container
            .decode(String.self, forKey: .colorSpace)
        let components = try container
            .decode([CGFloat].self, forKey: .components)
        
        guard
            let cgColorSpace = CGColorSpace(name: colorSpace as CFString),
            let cgColor = CGColor(
                colorSpace: cgColorSpace, components: components
            )
        else {
            throw CodingError.wrongData
        }
        
        self.cgColor = cgColor
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        guard
            let colorSpace = cgColor.colorSpace?.name,
            let components = cgColor.components
        else {
            throw CodingError.wrongData
        }
              
        try container.encode(colorSpace as String, forKey: .colorSpace)
        try container.encode(components, forKey: .components)
    }
}

enum CodingError: Error {
    case wrongColor
    case wrongData
}

This custom type can then be used to encode and decode Core Graphics representation of our constant SwiftUI color.

let color = Color(.sRGB, red: 1, green: 0, blue: 0.5, opacity: 1)

func encodeColor() throws -> Data {
    guard let cgColor = color.cgColor else {
        throw CodingError.wrongColor
    }
    return try JSONEncoder()
        .encode(CodableColor(cgColor: cgColor))
}

func decodeColor(from data: Data) throws -> Color {
    let codableColor = try JSONDecoder()
        .decode(CodableColor.self, from: data)
    return Color(cgColor: codableColor.cgColor)
}

The encoded data can be saved to a remote database, for example, and read on other platforms.

# Saving a color from ColorPicker

One of the most common scenarios when we need to encode and decode a constant color is when we want to save a color obtained from ColorPicker.

I wrote an example with UIColor/NSColor and an example with CGColor, you can get them both from GitHub.

ColorPicker can also accept a CGColor binding directly, if it's more convenient in your case.

You could go one step further and try to write the selected color into AppStorage. You can take a look at one of my previous articles Save Custom Codable Types in AppStorage or SceneStorage for ideas on how to approach it.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Black Friday 2024 offer: 30% off!$45$32

A detailed guide on gradually adopting SwiftUI in UIKit projects

Updated for iOS 18 and Xcode 16!

Integrating SwiftUI into UIKit Appsby Natalia Panferova

  • Upgrade your apps with new features like Swift Charts and Widgets
  • Support older iOS versions with effective backward-compatible strategies
  • Seamlessly bridge state and data between UIKit and SwiftUI using the latest APIs
Black Friday 2024 offer: 30% off!

A detailed guide on gradually adopting SwiftUI in UIKit projects

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Integrating SwiftUI
into UIKit Apps

by Natalia Panferova

Updated for iOS 18 and Xcode 16!

$45$32

# Encoding dynamic colors

Dynamic colors in SwiftUI are usually system colors that adapt to light and dark mode, accessibility settings and environment. They are a bit more complicated to handle because they change based on the context.

It's also more rare that we need to encode dynamic colors, but it can be useful in some cases. For example, we might be providing a predefined color palette consisting of system colors to the user to pick from, so that the app looks good in light and dark mode.

# Encoding system colors

We could convert a dynamic color to UIColor or NSColor and encode it using NSSecureCoding like we've seen with constant colors. But we should be aware that SwiftUI color created from a decoded UIColor or NSColor might not have the same functionality as the original system SwiftUI color.

One example that I know of where Color created from a system NSColor doesn't behave in the same way as the original SwiftUI Color is inside selected rows in the sidebar on the Mac.

NavigationView {
    List {
        NavigationLink("Row 1", destination: Text("Destination 1"))
        NavigationLink("Row 2", destination: Text("Destination 2"))
    }
    .foregroundColor(.primary)
    .listStyle(.sidebar)
}

In light mode Color.primary is dark, but when the row is selected it changes to white to increase contrast.

Screenshot of macOS sidebar in light mode where regular rows have dark text but selected row has blue background and white text.

Let's imagine that we encoded Color.primary by converting it to NSColor and decoded it back.

struct ContentView: View {
    @State private var color = Color.primary
    
    var body: some View {
        NavigationView {
            List {
                NavigationLink("Row 1", destination: Text("Destination 1"))
                NavigationLink("Row 2", destination: Text("Destination 2"))
            }
            .foregroundColor(color)
            .listStyle(.sidebar)
        }
        .task {
            do {
                let data = try await loadColorData()
                color = try await decodeColor(from: data)
            } catch {
                print("decoding error: \(error.localizedDescription)")
            }
        }
    }
    
    func decodeColor(from data: Data) async throws -> Color {
        guard let nsColor = try NSKeyedUnarchiver
                .unarchiveTopLevelObjectWithData(data) as? NSColor
            else {
                throw DecodingError.wrongType
            }
        return Color(nsColor: nsColor)
    }
    
    func loadColorData() async throws -> Data {
        ... // load color data
    }
}

In this case, primary color will stop being converted to white when row is selected.

Screenshot of macOS sidebar in light mode where all rows have dark text, even the selected one.

Depending on how decoded color is used in our app, we can still encode our dynamic colors via UIColor and NSColor for convenience. But if our decoded color doesn't behave how we expect it to, the conversion from UIColor/NSColor might be to blame.

A different way to encode a system SwiftUI color would be to define a Codable enum with all the cases that we support.

For example, if we are providing a palette that consists of Color.blue, Color.cyan and Color.indigo, we can write encoding and decoding logic as follows.

func encode(color: Color) throws -> Data {
    if let codableColor = CodableColor(color: color) {
        return try JSONEncoder().encode(codableColor)
    } else {
        throw EncodingError.wrongColor
    }
}

func decodeColor(from data: Data) throws -> Color {
    let codableColor = try JSONDecoder()
        .decode(CodableColor.self, from: data)
    return Color(codableColor: codableColor)
}

enum EncodingError: Error {
    case wrongColor
}

extension Color {
    init(codableColor: CodableColor) {
        switch codableColor {
        case .indigo: self = .indigo
        case .cyan: self = .cyan
        case .blue: self = .blue
        }
    }
}

enum CodableColor: Codable {
    case blue
    case cyan
    case indigo
    
    init?(color: Color) {
        switch color {
        case .blue: self = .blue
        case .cyan: self = .cyan
        case .indigo: self = .indigo
        default: return nil
        }
    }
}

You can take a look at this example code for saving system colors where user can pick and save a color from a provided palette.

# Encoding Colors from Assets

Another way to create dynamic colors in SwiftUI apps is to save them in Assets with different color for dark and light modes. In case we need to encode one of those colors and keep it adaptive, we can simply encode its name string. After decoding the name back, we can recreate the dynamic color.

func encode(colorName: String) throws -> Data {
    try JSONEncoder().encode(colorName)
}

func decodeColorName(from data: Data) throws -> String {
    try JSONDecoder().decode(String.self, from: data)
}

You can see the full example of saving colors from Assets on GitHub.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Black Friday 2024 offer: 30% off!$35$25

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms
Black Friday 2024 offer: 30% off!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35$25