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 arraylet 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 saveTasting
instances toUserDefaults
, but you can save JSON-/ or PropertyList-encodedTasting
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 lessNSObject
subclasses) unless necessary.I simplified some of the property names. E.g. in
Wish
, we wouldn’t generally usewish
prefix in property names. I also wouldn’t use “data” in a property name unless it was, in fact, aData
.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
How to Integrate .Proto Files in Xcode Compilation
How to Override Convenience Init in Uialertcontroller for Swift
Default Uifont Size and Weight But Also Support Preferredfontfortextstyle
Xcode 8.3 Swift Version Error (Swift_Version) in Objective C Project
Map Object into 2D Array Swift for Tableview Sections
Filemanager and Urlsfordirectory Error in Swift 3 Xcode 8
Uicollectionview - Horizontal Paging with One Cell at a Time
iOS Today Extension with Core Data
Swift Uiview Opacity Programmatically
Perform "Use Photo" Button on Custom Image Picker Overlay
Initialization of 'Unsafepointer<Int>' Results in a Dangling Pointer
Nsurlprotocol Isn't Asked to Load After Yes Response to Caninitwithrequest
Wrong Text Height When Text Contains Emoji
Problems with Layout of Some Rows in Swiftui List
How to Schedule Mail Using Gmail API