如何将UIImage转换为Codable?

42

Swift 4有Codable,非常棒。但是UIImage默认情况下不符合它。我们该怎么做呢?

我尝试使用singleValueContainerunkeyedContainer

extension UIImage: Codable {
  // 'required' initializer must be declared directly in class 'UIImage' (not in an extension)
  public required init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let data = try container.decode(Data.self)
    guard let image = UIImage(data: data) else {
      throw MyError.decodingFailed
    }

    // A non-failable initializer cannot delegate to failable initializer 'init(data:)' written with 'init?'
    self.init(data: data)
  }

  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    guard let data = UIImagePNGRepresentation(self) else {
      return
    }

    try container.encode(data)
  }
}

我收到了2个错误

  1. 'required'初始化器必须直接在类 'UIImage' 中声明(而不是在扩展中)
  2. 非可失败初始化器不能委托给使用'init?'编写的可失败初始化器'init(data:)'

一种解决方法是使用包装器。但还有其他方法吗?


3
如果您创建了一个继承自UIImage的子类,并符合Codable协议,然后在其上添加所需的初始化程序。 - Torongo
1
你为什么想要将 UIImage 转换为 Codable?通常来说,图片并不适合被编码成 JSON 或 XML 等格式。一般来说最好是将图片单独编码,然后在 JSON 中编码一个 URL 之类的东西。 - Hamish
1
如果您需要将图像保存在JSON字符串中,只需将图像数据转换为base64字符串,然后将其保存为字符串。 - Leo Dabus
1
@Hamish @LeoDabus 我的问题中没有提到 JSON 或 XML。我想你们是在建议使用 JSONEncoder?但这只是 Encoder 协议的一个实现。 - onmyway133
1
@onmyway133 我的主要问题只是想问你为什么需要这个 :) 其余部分是基于当前(和常用的)由Foundation提供的编码器/解码器的假设。 - Hamish
8个回答

42

正确的做法是将属性 Data 替换为 UIImage,代码如下:

Data 代替 UIImage

public struct SomeImage: Codable {

    public let photo: Data
    
    public init(photo: UIImage) {
        self.photo = photo.pngData()!
    }
}

反序列化图像:

UIImage(data: instanceOfSomeImage.photo)!

看起来真的很好!! - Malwinder Singh
一般来说,这不是一个好方法,因为你需要确保两个属性Data和相应的UIImage是同步的...否则你可能会得到数据与图像不匹配的情况,反之亦然...此外,在更多的属性上使用这个方法会增加相当多的样板代码...建议使用属性包装器方法。 - Peter Lapisu
好的,那么它缺少更新属性的支持,所以如果您将其更改为var,就会遇到我上面描述的问题...这只是一个过于基本的方法。 - Peter Lapisu

33

解决方案:创建符合 Codable 规范的自定义包装类。

一种解决方案是,由于扩展 UIImage 是不被允许的,所以可以将图像包装在一个您拥有的新类中。否则,您的尝试基本上是正确的。我在一个名为 Hyper Interactive 的缓存框架中看到了这个漂亮的实现,叫做Cache

尽管您需要访问库来深入了解依赖关系,但可以从查看他们的ImageWrapper类中获得灵感,其构建方式如下:

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

这是他们的包装类:

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

我很想知道你最终使用了什么。

更新: 结果发现 原帖作者编写了代码 我引用了他的代码(升级到Swift 4.0版本的Cache),当然这份代码值得放在这里,但是出于戏剧性的讽刺,我也会保留原话。 :)


13
谢谢。你知道我已经实施了那个功能,请查看提交记录。 - onmyway133
2
啊,我现在肯定知道了!我从没想到过这个世界会如此之小。吸取教训。嗯,这也意味着“我的”答案是被接受的,是吗? - AmitaiB
1
嗨,我想知道是否有比我的更聪明的解决方案。 - onmyway133
2
就我个人而言,我建议抛出 DecodingError.dataCorruptedError(forKey:in:debugDescription:) 等错误。 - Rob
1
感谢介绍这个包装器,非常好用。我想看看编码器/解码器在这种情况下的容器是如何工作的。 - Zhou Haibo
我认为这是错误的。UIImage不仅仅是它的数据。特别是,通过丢弃“比例”信息,您可能会反序列化与您序列化的非常不同的图像。 - matt

20

你可以使用扩展 KeyedDecodingContainerKeyedEncodingContainer 类非常优雅的解决方案:

enum ImageEncodingQuality {
  case png
  case jpeg(quality: CGFloat)
}

extension KeyedEncodingContainer {
  mutating func encode(
    _ value: UIImage,
    forKey key: KeyedEncodingContainer.Key,
    quality: ImageEncodingQuality = .png
  ) throws {
    let imageData: Data?
    switch quality {
    case .png:
      imageData = value.pngData()
    case .jpeg(let quality):
      imageData = value.jpegData(compressionQuality: quality)
    }
    guard let data = imageData 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: KeyedDecodingContainer.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")
      )
    }
  }
}

PS:您可以使用这种方式将 Codable 用于任何类类型


2
这是一个很棒的答案!+1 不过我会将 ImageEncodingQuality 枚举更改为 enum ImageType { case png; case jpeg(CGFloat) } - freytag
@freytag 你好像已经能读懂我的想法了,我在我的项目中已经转用自定义枚举类型来表示ImageType啦 :-) - Vitalii Gozhenko
3
不错!但您应该使用“DecodingError”和“EncodingError”。 - user652038
1
我建议不要将其存储为JPG格式...在这种情况下,每次保存都会降低质量,即使图像没有改变... - Peter Lapisu
1
@PeterLapisu,我完全同意你的观点,这就是为什么有两个选项:PNG和JPEG。当你想要通过网络传输一次并且质量对你不是很重要,但节省流量很重要时,JPEG可能会很有用。 - Vitalii Gozhenko

7

将UIImage传递的一种方法是将其转换为符合Codable协议的内容,例如String。

要在func encode(to encoder: Encoder) throws中将UIImage转换为String:

let imageData: Data = UIImagePNGRepresentation(image)!
let strBase64 = imageData.base64EncodedString(options: .lineLength64Characters)
try container.encode(strBase64, forKey: .image)

required init(from decoder: Decoder) throws 中将字符串转换回 UIImage:

let strBase64: String = try values.decode(String.self, forKey: .image)
let dataDecoded: Data = Data(base64Encoded: strBase64, options: .ignoreUnknownCharacters)!
image = UIImage(data: dataDecoded)

4
Data 实现了 Codable 协议,所以您不需要转换为字符串 ;) https://developer.apple.com/documentation/foundation/data/2895337-encode - Axel Guilmin

6

最佳解决方案是使用自定义属性包装器

  • 属性仍然可变
  • 无需更改代码,只需添加@CodableImage前缀

用法

class MyClass: Codable {
    
    @CodableImage var backgroundImage1: UIImage?
    @CodableImage var backgroundImage2: UIImage?
    @CodableImage var backgroundImage3: UIImage?

将以下代码添加到项目中:
@propertyWrapper
public struct CodableImage: Codable {
    
    var image: UIImage?
    
    public enum CodingKeys: String, CodingKey {
        case image
    }
    
    public init(image: UIImage?) {
        self.image = image
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let b = try? container.decodeNil(forKey: CodingKeys.image), b {
            
            self.image = nil
            
        } else {
    
            let data = try container.decode(Data.self, forKey: CodingKeys.image)
            
            guard let image = UIImage(data: data) else {
                throw DecodingError.dataCorruptedError(forKey: CodingKeys.image, in: container, debugDescription: "Decoding image failed")
            }
            
            self.image = image
        
        }
        
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        if let data = image?.pngData() {
            try container.encode(data, forKey: CodingKeys.image)
        } else {
            try container.encodeNil(forKey: CodingKeys.image)
        }
    }
    
    public init(wrappedValue: UIImage?) {
        self.init(image: wrappedValue)
    }

    public var wrappedValue: UIImage? {
        get { image }
        set {
            image = newValue
        }
    }
    
}

5

现有的答案似乎都是不正确的。如果您将反序列化的图像与原始图像进行比较,您会发现它们在任何意义上都可能不相等。这是因为所有答案都丢弃了 scale 信息。

您必须编码图像的 scale 和它的 pngData()。然后在解码 UIImage 时,通过调用 init(data:scale:) 将数据与比例组合。


1
请参见 https://dev59.com/I8Hqa4cB1Zd3GeqPuzZY#68137443,其中有一个可编码图像包装器的实际示例,该示例通过相等性测试。 - matt
你还应该注意UIImage的旋转数据(特别是如果这些图片是用iOS相机拍摄的)。pngData不会捕获这个。例如:https://stackoverflow.com/a/66588264/1271826。 - Rob
谢谢,这真的节省了我很多调试时间。 - Wizard

0

还有一个简单的解决方案,可以在图像上使用lazy var:

var mainImageData: Data {
    didSet { _ = mainImage }
}
lazy var mainImage: UIImage = {
    UIImage(data: mainImageData)!
}()

这样,在对象初始化和赋值给mainImageData时,它的didSet会启动,然后启动UIImage的初始化。

由于UIImage的初始化消耗大量资源,我们将它们捆绑在一起。 只需注意整个初始化将在后台线程上进行。


请注意,这可能导致数据和图像不相互对应...例如,mainImageData发生更改,但mainImage仍然指向旧图像。 - Peter Lapisu
谢谢@PeterLapisu,这只是一个例子,还有很多边缘情况。 - OhadM

0

Swift 5.4

// MARK: - ImageWrapper

public struct ImageWrapper: Codable {

    // Enums

    public enum CodingKeys: String, CodingKey {
        case image
    }

    // Properties

    public let image: UIImage

    // Inits

    public init(image: UIImage) {
        self.image = image
    }

    // Methods

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let data = try container.decode(Data.self, forKey: CodingKeys.image)
        if let image = UIImage(data: data) {
            self.image = image
        } else {
            // Error Decode
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        if let imageData: Data = image.pngData() {
            try container.encode(imageData, forKey: .image)
        } else {
            // Error Encode
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接