Using @AppStorage with SwiftUI Colors and some NSKeyedArchiver Magic

Conforming to RawRepresentable using a NSKeyedArchiver

Zane Carter
Jan 24 · 5 min read
Image for post
Image for post
Photo by Yousef Espanioly on Unsplash

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.

@AppStorage Documentation

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.

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
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)

Original Gist

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.

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! 🎉🎉🎉

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 🐦.

Code Story

Sharing everything we know about design and development.

Thanks to The Startup

Zane Carter

Written by

I make apps for the AppStore. Currently working on an app that makes gardening easier. Twitter: @iamzanecarter

Code Story

Sharing everything we know about design and development. A new publication by The Startup (https://medium.com/swlh).

Zane Carter

Written by

I make apps for the AppStore. Currently working on an app that makes gardening easier. Twitter: @iamzanecarter

Code Story

Sharing everything we know about design and development. A new publication by The Startup (https://medium.com/swlh).

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store