3

Is it possible to implement the Encodable and Decodable properties for UIColor

When I try to add a Decodable extension I get an error

extension UIColor : Decodable {
    public required init(from decoder: Decoder) throws {
        self.init(red: 1, green: 1, blue: 1, alpha: 1)
    }
}

error: ColorStuff.playground:98:21: error: initializer requirement 'init(from:)' can only be satisfied by a required initializer in the definition of non-final class 'UIColor' public required init(from decoder: Decoder) throws {

Am I missing something obvious here?

I have no issues with the Encodable extension - it seems its a Decodable issue.

The error message implies to me that I cannot do this due to not having access to the UIColor class definition

Scriptable
  • 19,402
  • 5
  • 56
  • 72
Jeef
  • 26,861
  • 21
  • 78
  • 156
  • 2
    Compare https://stackoverflow.com/questions/46522572/initializer-requirement-initjson-can-only-be-satisfied-by-a-required-init. – Martin R Feb 01 '18 at 15:40
  • 1
    You're missing this obvious error: ```'required' initializer must be declared directly in class 'UIColor' (not in an extension)``` – user28434'mstep Feb 01 '18 at 15:41
  • Yea thats what i was afraid of - as said by last sentence. So we'll call it a resounding NO WAY JOSE – Jeef Feb 01 '18 at 15:45
  • You can always attempt to [subclass](https://stackoverflow.com/questions/21212513/how-to-inherit-from-uicolor-correctly-or-how-to-access-uicolor-subclass-property) it and have the subclass implement `Codable`. Or perhaps write a wrapper for it. –  Feb 01 '18 at 16:06

2 Answers2

15

You cannot make UIColor conform to Decodable in an extension because of the error given by the compiler.

One solution is to make a Codable wrapper type and use that instead.

Since UIColor already conforms to NSCoding, let's just write a generic type so we can encode and decode anything that conforms to NSCoding.

import UIKit

struct WrapperOfNSCoding<Wrapped>: Codable where Wrapped: NSCoding {
    var wrapped: Wrapped

    init(_ wrapped: Wrapped) { self.wrapped = wrapped }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)
        guard let object = NSKeyedUnarchiver.unarchiveObject(with: data) else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "failed to unarchive an object")
        }
        guard let wrapped = object as? Wrapped else {
            throw DecodingError.typeMismatch(Wrapped.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "unarchived object type was \(type(of: object))"))
        }
        self.wrapped = wrapped
    }

    func encode(to encoder: Encoder) throws {
        let data = NSKeyedArchiver.archivedData(withRootObject: wrapped)
        var container = try encoder.singleValueContainer()
        try container.encode(data)
    }
}

let colors = [UIColor.red, UIColor.brown]
print(colors)
let jsonData = try! JSONEncoder().encode(colors.map({ WrapperOfNSCoding($0) }))
let colors2 = try! JSONDecoder().decode([WrapperOfNSCoding<UIColor>].self, from: jsonData).map({ $0.wrapped })
print(colors2)
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • If I try to use your (excellent) wrapper with a single value it throws an error when encoding - ie - jsonData = try! JSONEncoder().encode([w]) succeeds let jsonData = try! JSONEncoder().encode(w) fails with let w = WrapperOfNSCoding(colors[0]) error is Fatal error: 'try!' expression unexpectedly raised an error: Swift.EncodingError.invalidValue(WrapperOfNSCoding #1... Why would that be – daven11 Mar 16 '18 at 23:08
  • I will be happy to help you if you post your own top-level question. Feel free to add a comment here linking to it if you do. – rob mayoff Mar 16 '18 at 23:09
  • 1
    Thanks very much - https://stackoverflow.com/questions/49330725/wrapperofnscoding-fails-with-single-value – daven11 Mar 16 '18 at 23:15
1

There is as way to make UIColor Codable despite that required init. You can extend Codable itself, so that UIColor starts conforming it automatically.

import UIKit

extension Decodable where Self: NSSecureCoding {

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)
        guard let object = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Self else {
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: "Invalid object"
            )
        }
        self = object
    }
}

extension Encodable where Self: NSSecureCoding {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        let data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true)
        try container.encode(data)
    }
    
}

extension UIColor: Codable { }

Check it

import XCTest
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)
    }
}
CodingTextCase.defaultTestSuite.run()

But please note that in this case data for instance of UIColor will take about 5 hundred bytes and storing it as RGBA can be more compact.

Paul B
  • 3,989
  • 33
  • 46