'Make UIColor Codable
struct Task: Codable {
var content: String
var deadline: Date
var color: UIColor
...
}
There are warnings saying "Type 'Task' does not conform to protocol 'Decodable'" and "Type 'Task' does not conform to protocol 'Encodable'". I searched and found that this is because UIColor does not conform to Codable. But I have no idea how to fix that. So...
How to make UIColor Codable?
Solution 1:[1]
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
...
}
Solution 2:[2]
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.
Solution 3:[3]
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.
Solution 4:[4]
I solved this issue with a custom class that allowed automatic conformance to codable. This is beneficial as it prevents writing custom conformance to codable. It also makes it easier to work with UIColor and and CGColor
class Color:Codable{
private var _green:CGFloat
private var _blue:CGFloat
private var _red:CGFloat
private var alpha:CGFloat
init(color:UIColor) {
color.getRed(&_red, green: &_green, blue: &_blue, alpha: &alpha)
}
var color:UIColor{
get{
return UIColor(red: _red, green: _green, blue: _blue, alpha: alpha)
}
set{
newValue.getRed(&_red, green:&_green, blue: &_blue, alpha:&alpha)
}
}
var cgColor:CGColor{
get{
return color.cgColor
}
set{
UIColor(cgColor: newValue).getRed(&_red, green:&_green, blue: &_blue, alpha:&alpha)
}
}
}
Solution 5:[5]
We can make UIColor and all of its descendants Codable
.
import UIKit
extension Decodable where Self: UIColor {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let components = try container.decode([CGFloat].self)
self = Self.init(red: components[0], green: components[1], blue: components[2], alpha: components[3])
}
}
extension Encodable where Self: UIColor {
public func encode(to encoder: Encoder) throws {
var r, g, b, a: CGFloat
(r, g, b, a) = (0, 0, 0, 0)
var container = encoder.singleValueContainer()
self.getRed(&r, green: &g, blue: &b, alpha: &a)
try container.encode([r,g,b,a])
}
}
extension UIColor: Codable { }
Check it
import XCTest
class ColorDescendant: UIColor { }
let testColor = ColorDescendant.green
class CodingTextCase: XCTestCase {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
func testUIColor() throws {
let colorAsJSON = try encoder.encode(UIColor.red)
print(String(data: colorAsJSON, encoding: .utf8)!)
let uiColor = try? decoder.decode(UIColor.self, from: colorAsJSON)
XCTAssertEqual(uiColor!, UIColor.red)
}
func testUIColorDescendant() throws {
let colorAsJSON = try encoder.encode(testColor)
print(String(data: colorAsJSON, encoding: .utf8)!)
let uiColor = try? decoder.decode(ColorDescendant.self, from: colorAsJSON)
XCTAssertEqual(uiColor!, testColor)
}
}
CodingTextCase.defaultTestSuite.run()
This solution requires only 9 bytes for data storage while more generalized one will require about 500 bytes.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | |
Solution 4 | Shadrach Mensah |
Solution 5 |