I've been doing some tests on my models to make sure they are equal when I encode them into JSON and then decode them back using JSONEncoder/Decoder
. However, one of my tests failed, and the culprit was UIImage
. I've made sure that no errors were thrown during the encoding/decoding process.
First of all, this is the test in question:
func testProfileImageCodable() throws {
let image = ProfileImage(UIImage(systemName: "applelogo")!)
try XCTAssertTrue(assertCodable(image))
}
Here's my "Codability" test, where I make sure that types are equal before and after encoding/decoding:
func assertCodable<T: Codable & Equatable>(
_ value: T,
decoder: JSONDecoder = .init(),
encoder: JSONEncoder = .init()
) throws -> Bool {
let encoded = try encoder.encode(value)
let decoded = try decoder.decode(T.self, from: encoded)
return value == decoded
}
Firstly, here's how I made UIImage
work with Codable
:
extension KeyedEncodingContainer {
mutating func encode(_ value: UIImage, forKey key: Key) throws {
guard let data = value.pngData() else {
throw EncodingError.invalidValue(
value,
EncodingError.Context(codingPath: [key],
debugDescription: "Failed convert UIImage to data")
)
}
try encode(data, forKey: key)
}
}
extension KeyedDecodingContainer {
func decode(_ type: UIImage.Type, forKey key: Key) throws -> UIImage {
let imageData = try decode(Data.self, forKey: key)
if let image = UIImage(data: imageData) {
return image
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(codingPath: [key],
debugDescription: "Failed load UIImage from decoded data")
)
}
}
}
The UIImage
lives in a ProfileImage
type, so conforming it to Codable
looks like this:
extension ProfileImage: Codable {
enum CodingKeys: CodingKey {
case image
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.image = try container.decode(UIImage.self, forKey: .image)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.image, forKey: .image)
}
}
Furthermore, ProfileImage
's Equatable
conformance uses isEqual(_:)
on the UIImage
property, which they say is "the only reliable way to determine whether two images contain the same image data."
Yet, my test still fails, and I'm not sure why. Any help would be greatly appreciated.
the only reliable way to determine whether two images contain the same image data
They are wrong about that. That piece of the docs has misled me in the past too!
The way to compare two images for equality of content (the underlying bitmap) is to compare their pngData
.
What's wrong with your code, however, at the deepest level, is that a UIImage has scale
information which you are throwing away. For example, your original image's scale
is probably 2 or 3. But when you call image(data:)
on decoding, you fail to take that into account. If you did take it into account, your assertion would work as you expect.
I tweaked your code like this (there might be a better way, I just wanted to prove that scale
was the issue):
struct Image: Codable {
let image:UIImage
init(image:UIImage) {
self.image = image
}
enum CodingKeys: CodingKey {
case image
case scale
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let scale = try container.decode(CGFloat.self, forKey: .scale)
let image = try container.decode(UIImage.self, forKey: .image)
self.image = UIImage(data:image.pngData()!, scale:scale)!
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.image, forKey: .image)
try container.encode(self.image.scale, forKey: .scale)
}
}
Here's my test:
let im = UIImage(systemName:"applelogo")!
let encoded = try! JSONEncoder().encode(Image(image:im))
let decoded = try! JSONDecoder().decode(Image.self, from: encoded)
assert(im.pngData()! == decoded.image.pngData()!)
print("ok") // yep