32
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
  • 481
  • 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

6 Answers6

36

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 MyTask: Codable { // renamed as MyTask to avoid interference with Swift Concurrency
    
    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 = MyTask(content: "Foo", deadline: Date(), color: .orange)
do {
    let data = try JSONEncoder().encode(task)
    print(String(data: data, encoding: .utf8)!)
    let newTask = try JSONDecoder().decode(MyTask.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 MyTask: Codable {
    var content: String
    var deadline: Date
    @CodableColor var color: UIColor
...
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • 1
    Wow, I mean great. It was great idea to store different (`Color`) struct value as `Task'`'s codable protocol's UIColor property. – Prashant Tukadiya Aug 11 '18 at 05:14
  • Thanks for the code and example! – infinity_coding7 Aug 19 '22 at 20:21
  • any solution using propertyWrapper when I have array of UIColor ? – Rashesh Bosamiya Jun 07 '23 at 08:01
  • @RasheshBosamiya Yes, of course, just replace the single objects with the array equivalents and replace `unarchivedObject(ofClass:from:)` with `unarchivedObject(ofClasses:from:)` adding `NSArray.self` – vadian Jun 07 '23 at 09:00
  • thank you @vadian, it is working. just changed following `var wrappedValue: [UIColor]` & `NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: UIColor.self, from: data)` **However this is perfect solution in Swifty way.** – Rashesh Bosamiya Jun 09 '23 at 05:59
15

Here's a solution which I've published as a Swift Package which will work for any color in any color space (even fancy system colors like label and windowBackground!), and any other NSCoding object!

It's relatively easy to use:

import SerializationTools


let color = UIColor.label

let encodedData = try color.codable.jsonData()
// UTF-8 encoded Base64 representation of the `NSCoding` data

let decodedColor = try UIColor.CodableBridge(jsonData: encodedData).value

And remember that this even works with the fancy magical colors like .label and .systemBackground!

Of course, you can also use it like any other Swift codable, such as placing it in a struct with auto-synthesized Codable conformance or using it with JSONEncoder/JSONDecoder:

import SerializationTools


struct Foo: Codable {
    let color: UIColor.CodableBridge

    init(color: UIColor) {
        self.color = color.codable
    }
}
import SerializationTools


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, too, as well as anything else that conforms to NSCoding, such as NSImage/UIImage, MKMapView, GKAchievement, and much more!

Ky -
  • 30,724
  • 51
  • 192
  • 308
  • 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
  • 1
    @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
  • @fruitcoder So I've done all I can do. It seems that, of course, the callback function in `init(dynamicProvider:)` is not serialized However, other approaches to dynamic colors, such as a proper subclass of `UIColor` which implements the `NSCoding` requirements, work just fine! So hopefully that helps – Ky - Dec 28 '22 at 09:42
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
  • 1,022
  • 16
  • 19
  • 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
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.

Paul B
  • 3,989
  • 33
  • 46
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

Here's a generic version of vadjan:s smart solution with property wrappers. It can be used to make any NSCodable type conform to Codable.

@propertyWrapper
struct CodableByArchive<T>  where T:NSObject, T: NSCoding {
    var wrappedValue: T
}

extension CodableByArchive: Codable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true)
        try container.encode(data)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)
        guard let value = try NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: data) else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid decoding of archived data")
        }
        wrappedValue = value
    }
}

struct User: Codable {
    let name: String
    @CodableByArchive var favouriteColor: UIColor
    @CodableByArchive var photo: UIImage
}