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.

Tags:

Xcode9

Swift4