'Initializer requirement 'init(json:)' can only be satisfied by a `required` initializer in the definition of non-final class 'UIColor'

I'm trying to write an extension to satisfy a protocol in an extension like so:

extension UIColor: JSONRepresentable {
    convenience init?(json: Any) {
        guard let colourArray = json as? [CGFloat] else {
            print("json was not an array of CGFloats")
            return nil
        }
    
        self.init(
            red: colourArray[0],
            green: colourArray[1],
            blue: colourArray[2],
            alpha: colourArray[3]
        )
    }
}

I'm getting this error:

Initializer requirement 'init(json:)' can only be satisfied by a required initializer in the definition of non-final class 'UIColor'.

If I add a required keyword, I get this error

'required' initializer must be declared directly in class 'UIColor' (not in an extension).

Is there a reason for this or any way to work around it?

Edit: Just to be clear, here's the protocol

protocol JSONRepresentable {
    init?(json: Any)
}


Solution 1:[1]

struct Color: Codable {
    let red, green, blue, alpha: CGFloat
}

extension Color {
    var uiColor: UIColor { return UIColor(color: self) }
    var cgColor: CGColor { return uiColor.cgColor }
    var ciColor: CIColor { return CIColor(color: uiColor) }
    var data: Data { return try! JSONEncoder().encode(self) }
}

extension UIColor {
    convenience init(color: Color) {
        self.init(red: color.red, green: color.green, blue: color.blue, alpha: color.alpha)
    }
    var color: Color {
        let color = CIColor(color: self)
        return Color(red: color.red, green: color.green, blue: color.blue, alpha: color.alpha)
    }
}
extension Data {
    var string: String {
        return String(data: self, encoding: .utf8) ?? ""
    }
}

Playground testing

let json = """
{"red": 0.5, "green": 0.0, "blue": 0.0, "alpha": 1.0}
"""

if let color = try? JSONDecoder().decode(Color.self, from: Data(json.utf8)) {
    print(color)                  // "Color(red: 0.5, green: 0.0, blue: 0.0, alpha: 1.0)\n"
    print(color.uiColor)          // "UIExtendedSRGBColorSpace 0.5 0 0 1\n
    print(color.data)         // "40 bytes\n"
    print(color.data.string)  // "{"red":0.5,"alpha":1,"blue":0,"green":0}\n"
}

let redColor = UIColor.red.color
let jsonData = redColor.data.string  // "{"red":1,"alpha":1,"blue":0,"green":0}"


If you need to work with your array of CGFloats you can override JSON Encoder and Decoder initializers:

extension Color {
    public init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        red   = try container.decode(CGFloat.self)
        green = try container.decode(CGFloat.self)
        blue  = try container.decode(CGFloat.self)
        alpha = try container.decode(CGFloat.self)
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(red)
        try container.encode(green)
        try container.encode(blue)
        try container.encode(alpha)
    }
}

Testing

let values: [CGFloat] = [0.5,0.0,0.0,1.0]
let jsonData = try JSONSerialization.data(withJSONObject: values) // 11 bytes
let json = jsonData.string   // "[0.5,0,0,1]"

do {
    let color = try JSONDecoder().decode(Color.self, from: jsonData)
    print(color)                  // "Color(red: 0.5, green: 0.0, blue: 0.0, alpha: 1.0)\n"
    print(color.uiColor)          // "UIExtendedSRGBColorSpace 0.5 0 0 1\n
    print(color.data)                                  // "11 bytes\n"
    print(color.data.string)                           // "[0.5,0,0,1]\n"
    let encodedData = try JSONEncoder().encode(color)  // 11 bytes
    print(encodedData == jsonData)                     // true
} catch {
    print(error)
}

Solution 2:[2]

The message I get for this code is (Swift 5.0)

Initializer requirement 'init(json:)' can only be satisfied by a 'required' initializer in non-final class 'MyColor'

Trying replacing

extension UIColor: JSONRepresentable

with

class MyColor: UIColor, JSONRepresentable

, I've the same message.

It means that the MyColor class can't be extended 'as this', but if you use the mentioned required keyword, all subclasses would be mandatory to implement this initializer.

For example, adding final to the class (=> final class MyColor: UIColor, JSONRepresentable) you can forgot the 'required' initializer, because you explicitly mention that there will never be subclasses for 'MyColor'. It's nice, it's the principle for protocols, to says (to other developers, but to you too) that some functions are required or not.

Solution 3:[3]

You can make UIColor (and all of its descendants) Codable and use embedded JSONEncoder/Decoder to handle it.

import UIKit

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 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 UIColor: Codable { }

Test it

class ColorDescendant: UIColor { }
let testColor = ColorDesedant.green

let encoder = JSONEncoder()
let decoder = JSONDecoder()

let colorAsJSON = try encoder.encode(testColor)
print(String(data: colorAsJSON, encoding: .utf8)!)
let uiColor = try? decoder.decode(ColorDesedant.self, from: colorAsJSON)
uiColor! == testColor

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 tontonCD
Solution 3