Using @AppStorage with SwiftUI Colors and some NSKeyedArchiver Magic
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
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)
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
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 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
We can perform the same process in reverse using
NSKeyedUnarchiver to get the
String back into a SwiftUI
Here’s the code I came up with
With the use of this extension you can easily set up a SwiftUI
ColorPicker 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
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
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
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
NSColorSpace. The words (expectedly)
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.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 🐦.