encoding/decoding 时 UIImage 不等价

UIImage not equivalent when encoding/decoding

我一直在对我的模型进行一些测试,以确保当我将它们编码为 JSON 然后使用 JSONEncoder/Decoder 将它们解码回来时它们是相等的。然而,我的一项测试失败了,罪魁祸首是UIImage。我已确保在 encoding/decoding 过程中没有出现任何错误。

首先,这是有问题的测试:

func testProfileImageCodable() throws {
    let image = ProfileImage(UIImage(systemName: "applelogo")!)
    try XCTAssertTrue(assertCodable(image))
}

这是我的“可编码性”测试,我在其中确保类型在 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
}

首先,这是我如何让 UIImageCodable 一起工作:

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

UIImage 属于 ProfileImage 类型,因此符合 Codable 看起来像这样:

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

此外,ProfileImageEquatable 一致性使用 isEqual(_:) on the UIImage property, which they say 是“确定两个图像是否包含相同图像数据的唯一可靠方法”。

然而,我的测试仍然失败,我不确定为什么。任何帮助将不胜感激。

the only reliable way to determine whether two images contain the same image data

他们错了。那篇文档在过去也误导了我!

比较两个图像内容(底层位图)是否相等的方法是比较它们的pngData


然而,在最深层次上,您的代码有什么问题是 UIImage 具有您丢弃的 scale 信息。例如,您的原始图像的 scale 可能是 2 或 3。但是当您在解码时调用 image(data:) 时,您没有考虑到这一点。如果您确实考虑到了它,您的断言将如您所愿。

我像这样调整了你的代码(可能有更好的方法,我只是想证明 scale 是问题所在):

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

这是我的测试:

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