swift4xcode9

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

  • 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
    ...
    }