Drop a SwiftUI `Color` into a Codable struct and the compiler tells you the bad news: it doesn't work out of the box. Apple's color type lacks `Codable` conformance, so saving to disk, syncing to a server, or stashing a `ColorPicker` choice in `AppStorage` requires custom bridging logic.
The solution hinges on a critical distinction: are you dealing with a constant color or a dynamic one? Constant colors—those built from RGB or HSB values—encode cleanly. Dynamic system colors that shift between light and dark mode need different handling, and you may lose their adaptive behavior in the process.
Two flavors of color, two strategies
Constant colors** are hand-built from sRGB/HSB triples or lifted from `UIColor`/`NSColor`. They don't react to environment changes. **Dynamic colors** are the system ones—`Color.blue`, `Color.primary`—that quietly flip between appearances based on light mode, dark mode, and accessibility settings.
The approach you pick depends on where the data needs to go.
| Use case | Encoding path | Output format | Cross-platform |
|---|---|---|---|
| Apple-only save (AppStorage, disk) | `UIColor`/`NSColor` + `NSSecureCoding` | Binary `Data` | No |
| Web sync, JSON APIs | `CGColor` + custom `Codable` struct | JSON | Yes |
| ColorPicker binding | Direct `CGColor` binding | N/A | N/A |
Encoding for Apple platforms
When you know colors will only travel between iOS and macOS devices, convert `Color` to a `PlatformColor` alias (`UIColor` on iOS, `NSColor` on macOS), then lean on `NSKeyedArchiver` and `NSSecureCoding`:
“`swift
Let platformColor = PlatformColor(color)
Return try NSKeyedArchiver.archivedData(
WithRootObject: platformColor,
RequiringSecureCoding: true
)
“`
Decoding reverses the path: `NSKeyedUnarchiver` pulls the object back, type-check it against `PlatformColor`, then reconstruct the SwiftUI `Color`. A small error case handles the edge where the unarchived object isn't what you expected.
Encoding for cross-platform use
Need that color in a web dashboard or a non-Apple client? Drop to `CGColor`. Read the `.cgColor` property on a constant `Color`, capture the color space name and component array, and wrap them in a tiny `Codable` struct:
“`swift
Struct CodableColor: Codable {
Let cgColor: CGColor
Enum CodingKeys: String, CodingKey { case colorSpace, components }
// encode: pull colorSpace.name and components from cgColor
// decode: rebuild CGColor from the stored strings and numbers
}
“`
A critical caveat: `.cgColor` returns `nil` for dynamic colors. Call it on `Color.blue` and you'll get nothing. This is the dividing line between a straightforward save and a headache.
`ColorPicker` can bind directly to a `CGColor`, which makes wiring a picker to persistent storage cleaner than working with the SwiftUI `Color` wrapper.
Our analysis
Who wins:** Developers building theme pickers, persisting brand colors, or syncing design tokens across platforms. The `CodableColor` wrapper gives you JSON that survives beyond Apple's ecosystem.
Who loses:** Anyone trying to save a dynamic system color and expecting it to keep adapting. You can encode it, but the decoded result is a static snapshot—not the live, context-aware original.
The trade-off:** `NSSecureCoding` is concise and preserves full quality, but the output is Apple-only and binary. `CGColor` + JSON is portable and human-readable, but you're locked into storing constant colors only.
For most iOS apps with no cross-platform ambitions, the platform-color approach is the right default. Reach for the `CGColor` JSON wrapper the moment your data has to outlive an Apple device.
Worked example: saving a ColorPicker choice
A notes app with a highlight color setting—user picks a shade and expects it back on next launch.
- Bind a `@State` variable to `ColorPicker(selection: $chosenColor)`, optionally binding through a `Binding<CGColor>` to store the lower-level type directly.
- On change, convert to `PlatformColor` and archive with `NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:)`.
- Write the `Data` to the documents directory, or wrap it for `AppStorage` using a `RawRepresentable` adapter.
- On launch, unarchive and rebuild the `Color`.
If colors need to travel beyond iOS and macOS, swap the archive step for `CodableColor` + `JSONEncoder`.
What to watch
Until Apple adds native `Codable` conformance to `Color`, custom bridging remains necessary. Watch SwiftUI and Foundation release notes—color serialization is a known gap. In the meantime, keep your wrapper thin, your error cases explicit, and treat any saved dynamic color as a snapshot, not a contract.
Bottom line
Choose `UIColor`/`NSColor` + `NSSecureCoding` for fast, Apple-only persistence. Choose `CGColor` + JSON for data that needs to cross platform boundaries. Build a thin `Codable` envelope either way so your color survives the trip.
Key takeaways
- SwiftUI's `Color` won't serialize itself; you pick the storage format.
- Use `UIColor`/`NSColor` with `NSSecureCoding` for fast, Apple-only saves.
- Use a `CodableColor` wrapper over `CGColor` for portable, JSON-friendly storage.
- Dynamic system colors can be saved but may not adapt like the originals.
- `ColorPicker` can bind directly to `CGColor`, simplifying persistence.
Frequently asked questions
Why won't a SwiftUI `Color` just encode itself like other value types?
Apple has not added built-in `Codable` conformance to `Color`. Putting one inside a Codable struct fails until you define your own conversion logic.
Can I store a ColorPicker choice in AppStorage?
Yes, but `AppStorage` needs simple or `RawRepresentable` values. You can wrap encoded `Data` and store that, or build a small `RawRepresentable` adapter around your `CodableColor`.
What's the difference between encoding a constant color vs. A dynamic color?
A constant color preserves its components exactly. A dynamic color—like a system color—decodes as a static snapshot that doesn't auto-switch between light and dark mode.
Is the CGColor approach safe for sending colors to a web backend?
Yes. Storing the color space name plus a numeric components array gives you plain JSON that any web client can read, though very wide-gamut color spaces need a client that understands them.
Where can I find ready-made code for this?
The linked GitHub samples cover both the `UIColor`/`NSColor` and `CGColor` approaches, plus patterns for saving custom Codable types into `AppStorage` or `SceneStorage`.