Make UIColor Codable
If you care only about the 4 color components this is a simple solution using a wrapper struct
struct Color : Codable {
var red : CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
var uiColor : UIColor {
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
init(uiColor : UIColor) {
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
}
}
In this case you have to write a custom initializer to convert the 4 color components from Color
to UIColor
and vice versa.
struct Task: Codable {
private enum CodingKeys: String, CodingKey { case content, deadline, color }
var content: String
var deadline: Date
var color : UIColor
init(content: String, deadline: Date, color : UIColor) {
self.content = content
self.deadline = deadline
self.color = color
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
content = try container.decode(String.self, forKey: .content)
deadline = try container.decode(Date.self, forKey: .deadline)
color = try container.decode(Color.self, forKey: .color).uiColor
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(content, forKey: .content)
try container.encode(deadline, forKey: .deadline)
try container.encode(Color(uiColor: color), forKey: .color)
}
}
Now you can encode and decode UIColor
let task = Task(content: "Foo", deadline: Date(), color: .orange)
do {
let data = try JSONEncoder().encode(task)
print(String(data: data, encoding: .utf8)!)
let newTask = try JSONDecoder().decode(Task.self, from: data)
print(newTask)
} catch { print(error) }
A smart alternative for Swift 5.1 and higher is a property wrapper
@propertyWrapper
struct CodableColor {
var wrappedValue: UIColor
}
extension CodableColor: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
guard let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid color"
)
}
wrappedValue = color
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true)
try container.encode(data)
}
}
and mark the property with @CodableColor
struct Task: Codable {
var content: String
var deadline: Date
@CodableColor var color: UIColor
...
}
I use UIColor
subclass
final class Color: UIColor, Decodable {
convenience init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let hexString = try container.decode(String.self)
self.init(hex: hexString)
}
}
Thus, there is no need for each class or structure to implement the functions of the Decodable
protocol. It seems to me that this is the most convenient way, especially when there can be many color parameters in one class or structure.
You can implement Encodable
in the same way if it's necessary.
Here's a solution which I've published as a GitHub gist which will work for any color in any color space:
/// Allows you to use Swift encoders and decoders to process UIColor
public struct CodableColor {
/// The color to be (en/de)coded
let color: UIColor
}
extension CodableColor: Encodable {
public func encode(to encoder: Encoder) throws {
let nsCoder = NSKeyedArchiver(requiringSecureCoding: true)
color.encode(with: nsCoder)
var container = encoder.unkeyedContainer()
try container.encode(nsCoder.encodedData)
}
}
extension CodableColor: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
let decodedData = try container.decode(Data.self)
let nsCoder = try NSKeyedUnarchiver(forReadingFrom: decodedData)
self.color = try UIColor(coder: nsCoder).unwrappedOrThrow()
// `unwrappedOrThrow()` is from OptionalTools: https://github.com/RougeWare/Swift-Optional-Tools
// You can use this if you don't want to use OptionalTools:
/*
guard let color = UIColor(coder: nsCoder) else {
struct UnexpectedlyFoundNilError: Error {}
throw UnexpectedlyFoundNilError()
}
self.color = color
*/
}
}
public extension UIColor {
func codable() -> CodableColor {
return CodableColor(color: self)
}
}
It's relatively easy to use:
let color = UIColor.label
let encoder = JSONEncoder()
let encodedData = try encoder.encode(color.codable())
let decoder = JSONDecoder()
let decodedColor = try decoder.decode(CodableColor.self, from: encodedData).color
Of course, you can also use it as any other Swift codable, like in a struct with auto-synthesized codable conformance:
struct Foo: Codable {
let color: CodableColor
init(color: UIColor) {
self.color = CodableColor(color: color)
}
}
let fooInstance = Foo(color: .systemPurple)
let encoder = JSONEncoder()
let encodedData = try encoder.encode(fooInstance)
let decoder = JSONDecoder()
let decodedFoo = try decoder.decode(Foo.self, from: encodedData)
This will work with NSColor
as well.