Implementing Codable for UIcolor

Implementing Codable for UIColor

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)

Make UIColor Codable

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

Making UIColor Codable - conforming to protocol 'Encodable'

Why do you want UIColor additionally conform to Codable? It conforms already to NSSecureCoding so it's serializable to Data by default.

And even with your implementation you can encode a color

let encoded = UIColor.Words.adverb.encode()!

and decode it

let color = UIColor.color(data: encoded)

And particularly in Core Data you can use computed properties to transform a supported type to an unsupported and vice versa.

My recommedation for Core Data is to save the color as Int32 (the hex representation) or as (hex) String


This is an implementation I'm using in Core Data, an extension to convert the color to a hex string

extension UIColor {

private func float2String(_ float : CGFloat) -> String {
return String(format:"%02X", Int(round(float * 255)))
}

var hex : String {
var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0
getRed(&red, green: &green, blue: &blue, alpha: nil)
return "#" + float2String(red) + float2String(green) + float2String(blue)
}

convenience init(hex : String) {
let hex = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex
if hex.count == 6, hex.range(of: "[^0-9A-Fa-f]", options: .regularExpression) == nil {
let chars = Array(hex)
let numbers = stride(from: 0, to: chars.count, by: 2).map() {
CGFloat(strtoul(String(chars[$0 ..< min($0 + 2, chars.count)]), nil, 16))
}
self.init(red: numbers[0] / 255, green: numbers[1] / 255, blue: numbers[2] / 255, alpha: 1.0)
} else {
self.init(white: 1.0, alpha: 1.0)
}
}
}

and the relevant part of the NSManagedObject subclass

@NSManaged public var hexColor: String

var color : UIColor {
get { return UIColor(hex: hexColor) }
set { hexColor = newValue.hex }
}

Make a built in type conform to codeable

One of the solutions is to make Color conform to Codable:

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)
}
}

This works if you're interested in RGB and alpha components only. If you need more, you can use this wrapper originally proposed here.

Note: You may need to explicitly implement Equatable and Hashable protocols as well.

How to conform UIImage to Codable?

A solution: roll your own wrapper class conforming to Codable.

One solution, since extensions to UIImage are out, is to wrap the image in a new class you own. Otherwise, your attempt is basically straight on. I saw this done beautifully in a caching framework by Hyper Interactive called, well, Cache.

Though you'll need to visit the library to drill down into the dependencies, you can get the idea from looking at their ImageWrapper class, which is built to be used like so:

let wrapper = ImageWrapper(image: starIconImage)
try? theCache.setObject(wrapper, forKey: "star")

let iconWrapper = try? theCache.object(ofType: ImageWrapper.self, forKey: "star")
let icon = iconWrapper.image

Here is their wrapper class:

// Swift 4.0
public struct ImageWrapper: Codable {
public let image: Image

public enum CodingKeys: String, CodingKey {
case image
}

// Image is a standard UI/NSImage conditional typealias
public init(image: Image) {
self.image = image
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let data = try container.decode(Data.self, forKey: CodingKeys.image)
guard let image = Image(data: data) else {
throw StorageError.decodingFailed
}

self.image = image
}

// cache_toData() wraps UIImagePNG/JPEGRepresentation around some conditional logic with some whipped cream and sprinkles.
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
guard let data = image.cache_toData() else {
throw StorageError.encodingFailed
}

try container.encode(data, forKey: CodingKeys.image)
}
}

I'd love to hear what you end up using.

UPDATE: It turns out the OP wrote the code that I referenced (the Swift 4.0 update to Cache) to solve the problem. The code deserves to be up here, of course, but I'll also leave my words unedited for the dramatic irony of it all. :)

Saving UIColor within Struct Array to UserDefaults

The errors are not related to my solution.

  • The first error tells you that the object is an array. Please read your code, tastings is clearly an array.

    So you have to decode an array

    let newTastings = try JSONDecoder().decode([Tasting].self, from: data)
  • The second error tells you that in your struct is a type which is not property list compliant. This type is UIColor. You cannot save Tasting instances to UserDefaults, but you can save JSON-/ or PropertyList-encoded Tasting instances.

     let data = try JSONEncoder().encode(tastings)
    UserDefaults.standard.set(data, forKey: "tastings")

Custom Struct: Type does not conform to protocol 'Decodable'

You will need to make Wish adopt Codable.

But because UIImage and UIColor are not Codable, you’ll have to manually implement them as outlined in Encoding and Decoding Custom Types:

struct Wishlist: Codable {
var name: String
var image: UIImage
var wishes: [Wish]
var color: UIColor
var textColor: UIColor
var index: Int

init(name: String, image: UIImage, wishes: [Wish], color: UIColor, textColor: UIColor, index: Int) {
self.name = name
self.image = image
self.wishes = wishes
self.color = color
self.textColor = textColor
self.index = index
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

name = try values.decode(String.self, forKey: .name)
wishes = try values.decode([Wish].self, forKey: .wishData)
color = try values.decode(Color.self, forKey: .color).uiColor
textColor = try values.decode(Color.self, forKey: .textColor).uiColor
index = try values.decode(Int.self, forKey: .index)

let data = try values.decode(Data.self, forKey: .image)
guard let image = UIImage(data: data) else {
throw DecodingError.dataCorruptedError(forKey: .image, in: values, debugDescription: "Invalid image data")
}
self.image = image
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(name, forKey: .name)
try container.encode(wishes, forKey: .wishData)
try container.encode(Color(uiColor: color), forKey: .color)
try container.encode(Color(uiColor: textColor), forKey: .textColor)
try container.encode(index, forKey: .index)
try container.encode(image.pngData(), forKey: .image)
}

}

struct Wish: Codable {
public var name: String
public var checkedStatus: Bool
public var link: String
public var price: String
public var note: String
public var image: UIImage

init(name: String, link: String, price: String, note: String, image: UIImage, checkedStatus: Bool) {
self.name = name
self.checkedStatus = checkedStatus
self.link = link
self.price = price
self.note = note
self.image = image
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

name = try values.decode(String.self, forKey: .name)
checkedStatus = try values.decode(Bool.self, forKey: .checkedStatus)
link = try values.decode(String.self, forKey: .link)
price = try values.decode(String.self, forKey: .price)
note = try values.decode(String.self, forKey: .note)

let data = try values.decode(Data.self, forKey: .image)
guard let image = UIImage(data: data) else {
throw DecodingError.dataCorruptedError(forKey: .image, in: values, debugDescription: "Invalid image data")
}
self.image = image
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(name, forKey: .name)
try container.encode(checkedStatus, forKey: .checkedStatus)
try container.encode(link, forKey: .link)
try container.encode(price, forKey: .price)
try container.encode(note, forKey: .note)
try container.encode(image.pngData(), forKey: .image)
}
}

Where I’d use this as a convenient way to encode UIColor objects:

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

init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}

init(uiColor: UIColor) {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0

uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)

self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}

var uiColor: UIColor { UIColor(red: red, green: green, blue: blue, alpha: alpha) }
}

Note, I did a couple of unrelated changes:

  • I made both of these struct. I wouldn’t introduce reference types (much less NSObject subclasses) unless necessary.

  • I simplified some of the property names. E.g. in Wish, we wouldn’t generally use wish prefix in property names. I also wouldn’t use “data” in a property name unless it was, in fact, a Data.

  • I updated init methods to use standard naming conventions.


By the way, another approach is to avoid using UIKit types within model types at all. This eliminates the need for custom encoders/decoders at all. And you can have platform-specific extensions, that provide the necessary convenience initializers that bridge to the UIKit types. E.g.:

// MARK: - Wishlist

struct Wishlist: Codable {
let name: String
let imageData: Data // rather than `UIImage`
let wishes: [Wish]
let color: Color // rather than `UIColor`
let textColor: Color // rather than `UIColor`
let index: Int
}

// MARK: Wishlist UIKit extension

#if os(iOS)
extension Wishlist {
init(name: String, image: UIImage, wishes: [Wish], color: UIColor, textColor: UIColor, index: Int) {
self.init(
name: name,
imageData: image.pngData()!,
wishes: wishes,
color: Color(uiColor: color),
textColor: Color(uiColor: textColor),
index: index
)
}

var image: UIImage? { UIImage(data: imageData) }
}
#endif

// MARK: - Wish

struct Wish: Codable {
let name: String
let checkedStatus: Bool
let link: URL // rather than `String`
let price: String
let note: String
let imageData: Data // rather than `UIImage`
}

// MARK: Wish UIKit extension

#if os(iOS)
extension Wish {
init(name: String, link: URL, price: String, note: String, image: UIImage, checkedStatus: Bool) {
self.init(
name: name,
checkedStatus: checkedStatus,
link: link,
price: price,
note: note,
imageData: image.pngData()!
)
}

var image: UIImage? { UIImage(data: imageData) }
}
#endif

Note, I not only eliminated UIColor from the model types, but also UIImage, too. Now, above I shifted the UIImage to Data, but really, the image payload probably does not belong in this model type at all. You should just have image URLs or asset identifiers, and decouple the image fetch and storage from the model altogether. (Because images and model objects tend to have very different memory characteristics, you often want images fetched as they are needed and stored within some flushable cache. But that is beyond the scope of this question.)



Related Topics



Leave a reply



Submit