SwiftUI Color type does not 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 do not change based on context, do not have different light and dark appearance and do not 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
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
let dynamicColor = Color.blue
let cgColor2 = dynamicColor.cgColor
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 CGColoryou 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.
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
does not 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.
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
...
In this case, primary color will stop being converted to white when row is selected.

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 does not 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.
For updates about the blog and development tips follow us on Twitter @nilcoalescing.