Make UIColor Codable
Asked Answered
S

6

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

Suzan answered 19/6, 2018 at 12:12 Comment(6)
Possible duplicate of Implementing Codable for UIColorErastes
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.Prosecution
@Prosecution But I need to save the data thoughSuzan
@Erastes I tried before. It does not work for me.Suzan
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. hackingwithswift.com/example-code/language/…Labyrinth
@caminanteerrante I didn't think of NSCoding at all. This is so clean and straightforward. Thanks!Suzan
E
40

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
...
}
Erastes answered 19/6, 2018 at 18:30 Comment(5)
Wow, I mean great. It was great idea to store different (Color) struct value as Task''s codable protocol's UIColor property.Fronde
Thanks for the code and example!Orff
any solution using propertyWrapper when I have array of UIColor ?Frasier
@RasheshBosamiya Yes, of course, just replace the single objects with the array equivalents and replace unarchivedObject(ofClass:from:) with unarchivedObject(ofClasses:from:) adding NSArray.selfErastes
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.Frasier
G
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!

Grimbald answered 4/10, 2019 at 20:36 Comment(7)
Very helpful. I don't see a method codable() so in place of color.codable() I am using CodableColor(color: color)Aileenailene
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.Grimbald
Do you know if this works with "dynamic" colors? So colors that are created using the init(dynamicProvider:) initializer?Jaynajayne
@Jaynajayne No idea! I can test sometime later, but if you have any results to share before I get to it, do post them here 😁Grimbald
@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 knowJaynajayne
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!Grimbald
@Jaynajayne 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 helpsGrimbald
A
9

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.

Atcliffe answered 19/4, 2022 at 15:34 Comment(0)
W
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.

Willyt answered 10/12, 2018 at 19:15 Comment(3)
This seems to have broken in Xcode 10.2. Not sure if Swift 5 is the cause or not. Any idea?Intrigante
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.Cavy
This doesn't work due to class clusters. UIColor is actually just a disguise that represents many different internal classes not available to us.Kelcy
H
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)
    }
}

}

Horrified answered 24/4, 2019 at 16:45 Comment(1)
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!Atavism
S
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
}
Scintillometer answered 22/3, 2023 at 12:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.