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