Storing Colors in Core Data—The Whole Story
The task: Store colors (Color
, UIColor
and CGColor
) in Core Data, while remaining 100% compatible with the SwiftUI color picker.
Setting Up the Core Data Model
In your model add an attribute to your entity and set its type to Transformable. Select it and in the Data Model Inspector (right pane in Xcode, last tab) set the Transformer to “SerializableColorTransformer” and Custom Class to “SerializableColor”.
These two strings are magic values, and I’ll explain their origin later.
The whole thing should look like this:
Code Implementation
Core Data has several requirements on what can be stored. It has to be NSCoding
or NSSecureCoding
. This in turn means that it has to be an Obj-C class. This rules out both CGColor
(a non-Obj-c class) and Color
(a struct).
In my case I don’t care about color spaces other than P3 and sRGB. Let us create a class that stores red, green, blue and alpha components and a color space. The class will transform any color space other than P3 into sRGB.
Here is the whole listing for the class, which I called SerializableColor
. This is where the magic string for Custom Class in the previous section comes from.
// SerializableColor.swift
import Foundation
import struct CoreGraphics.CGFloat
import class CoreGraphics.CGColor
import class CoreGraphics.CGColorSpace
import class UIKit.UIColor
import struct SwiftUI.Color
public class SerializableColor: NSObject, NSCoding, NSSecureCoding {
public static var supportsSecureCoding: Bool = true
public enum SerializableColorSpace: Int {
case sRGB = 0
case displayP3 = 1
}
let colorSpace: SerializableColorSpace
let r: Float
let g: Float
let b: Float
let a: Float
public func encode(with coder: NSCoder) {
coder.encode(colorSpace.rawValue, forKey: "colorSpace")
coder.encode(r, forKey: "red")
coder.encode(g, forKey: "green")
coder.encode(b, forKey: "blue")
coder.encode(a, forKey: "alpha")
}
required public init?(coder: NSCoder) {
colorSpace = SerializableColorSpace(rawValue: coder.decodeInteger(forKey: "colorSpace")) ?? .sRGB
r = coder.decodeFloat(forKey: "red")
g = coder.decodeFloat(forKey: "green")
b = coder.decodeFloat(forKey: "blue")
a = coder.decodeFloat(forKey: "alpha")
}
init(colorSpace: SerializableColorSpace, red: Float, green: Float, blue: Float, alpha: Float) {
self.colorSpace = colorSpace
self.r = red
self.g = green
self.b = blue
self.a = alpha
}
convenience init(from cgColor: CGColor) {
var colorSpace: SerializableColorSpace = .sRGB
var components: [Float] = [0, 0, 0, 0]
// Transform the color into sRGB space
if cgColor.colorSpace?.name == CGColorSpace.displayP3 {
if let p3components = cgColor.components?.map({ Float($0) }),
cgColor.numberOfComponents == 4 {
colorSpace = .displayP3
components = p3components
}
} else {
if let sRGB = CGColorSpace(name: CGColorSpace.sRGB),
let sRGBColor = cgColor.converted(to: sRGB, intent: .defaultIntent, options: nil),
let sRGBcomponents = sRGBColor.components?.map({ Float($0) }),
sRGBColor.numberOfComponents == 4 {
components = sRGBcomponents
}
}
self.init(colorSpace: colorSpace, red: components[0], green: components[1], blue: components[2], alpha: components[3])
}
convenience init(from color: Color) {
self.init(from: UIColor(color))
}
convenience init(from uiColor: UIColor) {
self.init(from: uiColor.cgColor)
}
var cgColor: CGColor {
return uiColor.cgColor
}
var color: Color {
return Color(self.uiColor)
}
var uiColor: UIColor {
if colorSpace == .displayP3 {
return UIColor(displayP3Red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(a))
} else {
return UIColor(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(a))
}
}
}
// MARK: Transformer Class
// For CoreData compatibility.
@objc(SerializableColorTransformer)
class SerializableColorTransformer: NSSecureUnarchiveFromDataTransformer {
override class var allowedTopLevelClasses: [AnyClass] {
return super.allowedTopLevelClasses + [SerializableColor.self]
}
public override class func allowsReverseTransformation() -> Bool {
return true
}
public override func transformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else {return nil}
return try! NSKeyedUnarchiver.unarchivedObject(ofClass: SerializableColor.self, from: data)
}
public override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let color = value as? SerializableColor else {return nil}
return try! NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true)
}
}
The important parts are described as follows.
To register the transformer (explained below), add this line to the init()
method of your @main
. For example
@main
struct Eventail_v4App: App {
init() {
// ...
ValueTransformer.setValueTransformer(
SerializableColorTransformer(),
forName: NSValueTransformerName(
rawValue: "SerializableColorTransformer"))
}
// ...
}
The rawValue
is where the Transformer magic string in the model come from.
Deeper dive
Instead of commenting the file excessively, here are explanations of details.
NSCoding inheritance
For this we implement encode(with coder: NSCoder)
and init?(coder: NSCoder)
. These methods will enable the class to be serialized and deserialized using any standard decoder/encoder.
Note: As you can see this class will interpret any unknown color space as sRGB, this means that this is not backwards compatible.
Color space transformations
These happen in two places. When encoding the value, the class always passes through CGColor as this is the only color type that can be split into components.
Note: In init from
Color
I bounce through init fromUIColor
, this is because weirdlyColor.blue.cgColor == nil
butUIColor(Color.blue).cgColor
has a value.
For deserialization I always pass through UIColor
as this type has convenience initializers for both sRGB and P3 colorspaces.
Transformer
Core Data can write only a few basic types, including NSData
. Transformers are classes that can be registered for specific types and transform them into, and from NSData
.
In order to implement a transformer you need to do three things:
- Write a class which inherits from
NSSecureUnarchiveFromDataTransformer
and overrides theallowedTopLevelClasses
class variable adding your class type to the list - Override the
transformedValue
andreverseTransformedValue
methods. These are named as seen from CoreData perspective.transformedValue
transformsNSData
into your class, in this caseSerializableColor
.reverseTransformedValue
transforms the class intoNSData
. You do not need to implement this method if you are never writing to CoreData yourself. In this case change the return value ofallowsReverseTransformation
tofalse
.
- Register the transformer in your
@main
somewhere.