27
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?

Lambdalex
  • 431
  • 1
  • 5
  • 10
  • 1
    Possible duplicate of [Implementing Codable for UIColor](https://stackoverflow.com/questions/48566443/implementing-codable-for-uicolor) – vadian Jun 19 '18 at 12:32
  • I don't think that you need `UIColor` codable. Why not store what ever data you have for color and return color object from a method. – Desdenova Jun 19 '18 at 12:42
  • @Desdenova But I need to save the data though – Lambdalex Jun 19 '18 at 15:05
  • @vadian I tried before. It does not work for me. – Lambdalex Jun 19 '18 at 15:06
  • 2
    After searching a lot I found this link to be extremely useful for the purpose of making types that conform to NSCoding conform to Codable, in your case UIColor. It explains it and gives sample code that works like a charm. I have used for types like UIColor and CGPoint. https://www.hackingwithswift.com/example-code/language/how-to-store-nscoding-data-using-codable – caminante errante Dec 05 '19 at 19:52
  • @caminanteerrante I didn't think of NSCoding at all. This is so clean and straightforward. Thanks! – Lambdalex Dec 07 '19 at 12:09

5 Answers5

30

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
...
}
vadian
  • 253,546
  • 28
  • 306
  • 323
10

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.

Ky.
  • 28,753
  • 48
  • 180
  • 285
  • 1
    Very helpful. I don't see a method codable() so in place of color.codable() I am using CodableColor(color: color) – Scott Carter Mar 04 '20 at 15:55
  • Thanks for reminding me, @ScottCarter! I forgot to finish this off with an extension method. But yes, you're correct; that was the proper way to use it before the edit I just made. – Ky. Mar 04 '20 at 18:09
  • 2
    Do you know if this works with "dynamic" colors? So colors that are created using the `init(dynamicProvider:)` initializer? – fruitcoder Apr 11 '20 at 15:33
  • @fruitcoder No idea! I can test sometime later, but if you have any results to share before I get to it, do post them here – Ky. Apr 13 '20 at 18:08
  • @BenLeggiero the dynamic color gets lost in the encoding/decoding process and the light color is returned for both userInterfaceStyles :) quite obvious this wouldn't work, but good to know – fruitcoder Apr 14 '20 at 09:15
  • Thanks, @fruitcoder! Sounds about right. Might be able to add a flag to do a render step in this to get the current RGB value out of it if that's desired. Will put it on my to-do list! – Ky. Apr 14 '20 at 14:54
5

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.

Nikaaner
  • 718
  • 10
  • 13
  • This seems to have broken in Xcode 10.2. Not sure if Swift 5 is the cause or not. Any idea? – Smongo Apr 02 '19 at 09:36
  • 1
    I'm on Xcode 11.3.1 with Swift 5 and this seems broken to me too - I get `DecodingError.valueNotFound` when I try to decode it. – Secansoida Mar 31 '20 at 09:36
  • 1
    This doesn't work due to class clusters. UIColor is actually just a disguise that represents many different internal classes not available to us. – Connor Jan 26 '21 at 01:03
3

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

}

  • I had to change this just a little but to make it compatible with NSColor under swift 5. I'll post my modification. Thanks for the code! – SouthernYankee65 Mar 10 '22 at 21:58
0

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.

Paul B
  • 2,408
  • 21
  • 33