Using @AppStorage with SwiftUI Colors and some NSKeyedArchiver Magic
Conforming to RawRepresentable using a NSKeyedArchiver
iOS 14 came with some exciting new features for SwiftUI and one of my favorites is the @AppStorage
wrapper. This new property wrapper makes storing a value in UserDefaults
way easier. Prior to iOS 14, this would have to be done by using getter and setter methods.
The @AppStorage natively supports Bool
, Int
, String
, URL
& Data
. But what if you want to display more complicated data types? For example, a SwiftUI Color
. I came across this problem myself developing one of my own apps. I wanted to store a default tint color for certain content in UserDefaults
, selected using (also new with iOS 14) ColorPicker
.
To support a non-primitive or custom class, you must extend the type to conform to RawRepresentable
. The protocol specifies both a custom initializer for initializing a value from a raw type, and a public variable for converting an object instance to the raw type. To conform to this protocol, unfortunately, we can’t use some of the more advanced types like Data
. We are limited to using either a String
or Int
as our raw type.
No matter, using an integer as a type to store color is pretty common and has been done before. Unfortunately, it’s not as easy as it first seemed. My first thought was to use a single 64 Bit Integer to store the 4 color components as bytes in the following format RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA…
. To save to UserDefaults
, I would extract the color components from the SwiftUI Color
(By converting to CIColor
) and then shift the components into the correct parts of the integer.
If you don’t know what bit shifting is, here’s a quick rundown: https://en.wikipedia.org/wiki/Logical_shift
A 64 Bit Integer can natively be stored in UserDefaults
so the next step is to simply reverse the process and then reconstruct a new SwiftUI Color
object from the extracted components when needed. Tada! A pretty simple way to convert and store a Color
as a primitive (and back). Here’s a quick example implementation.
Converting to an Integer
let red = Int(coreImageColor.red * 255 + 0.5)
let green = Int(coreImageColor.green * 255 + 0.5)
let blue = Int(coreImageColor.blue * 255 + 0.5)
return (red << 16) | (green << 8) | blue
Converting back to a Color
let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF
let green = Double((rawValue & 0x00FF00) >> 8) / 0xFF
let blue = Double(rawValue & 0x0000FF) / 0xFF
self = Color(red: red, green: green, blue: blue)
Unfortunately, It wasn’t that simple. Extracting to components and back, often caused some floating-point errors. 0 values would often be represented as very small numbers which could result in some very strange unexpected behavior. I tried range locking the values (to 0 — 255) but I still got some very strange UI Behaviour.
What happened when I ran the app?
Not exactly a great user experience, right? Changing one slider changes the others. Even changing a single slider isn’t reliable. Close and kill the app task and the data isn’t even reliably maintained. Values would be slightly above or below the values selected on the picker. Values seemed to be correctly converted between the raw type and the color so what gives?
My guess is that shifting the bits to perform raw and color conversions, as well as converting to a CIColor
to extract raw color values causes memory corruption somewhere down the line.
I decided to instead try using the good ol’ NSKeyedArchiver
classes to archive an actual object. This is commonly used with UserDefaults
and it saves us the pain of interacting directly with the red, green, blue, and opacity values. We can’t natively encode a Color
because it isn’t an NSObject
but luckily a UIColor
is.
UIColor
also now has an initializer that takes a SwiftUI Color
as an argument. This makes converting between a SwiftUI Color
and a UIColor
a breeze. We simple convert from a Color
to a UIColor
, then use NSKeyedArchiver
to archive the object into Data
, and finally, we can Base64 this Data
to get it into a raw String
.
We can perform the same process in reverse using NSKeyedUnarchiver
to get the String
back into a SwiftUI Color
.
Here’s the code I came up with
With the use of this extension you can easily set up a SwiftUIColorPicker
that saves to UserDefaults
and maintains between launches. Here’s a quick example view.
And here’s the end result! 🎉🎉🎉
What’s the difference?
The advantage of using NSKeyedArchiver
is that there’s no messing around with any color components. Nothing we do (to an extent) can break. It’s all up to Apple’s NSKeyedArchiver
& NSKeyedUnarchiver
. We don’t have to do any bit shifting or bitwise arithmetic as all the conversion is handled for us. The downside is that the size when stored on a device is about 400 bytes
instead of 8 bytes
.
Why the huge difference? Well, remember that we’re not storing just the color values like in the original bit shifting method. We’re storing an actual instance of an NSObject
. The NSKeyedArchiver
will encode everything including the keys of the encoded object and any of its underlying types. This will take up a comparatively larger amount of memory but it means that conversions between the raw type are direct instead of going through a tricky and potentially more error-prone conversion process.
You can see below what the NSKeyedArchiver
outputs converted into ASCII. There’s a bunch of special characters but you can make out bits and pieces like ColorComponentCount
and NSColorSpace
. The words (expectedly) Red
, Green
, and Blue
also appear. If each character takes up a byte (assuming UTF-8 encoding) then we easily have a few hundred bytes.
Have a closer look and you’ll also see there are some actual values in here like 0.599
and 0.255
. It’s hard to see exactly how these correspond to the stored color without going deeper into the data with some tools, but it gives you a good idea of how thing’s get encoded in comparison with trying to smash everything into a single integer.
There may very well be a way to store a SwiftUI Color
in an integer or string without causing weird UI bugs. If someone has a method that works well and doesn’t cause strange UI behavior; please do share. But until then, I’m happy sacrificing a tiny bit more memory for significantly better performance and functionality.
I hope all this taught you something, I certainly learned a little. 🧠
And if you liked this article, give us a follow on Twitter 🐦.