如何使UIImage符合Codable?

时间:2017-09-13 12:36:52

标签: uiimage swift4 codable

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. 解决方法是使用包装器。但还有其他方法吗?

6 个答案:

答案 0 :(得分:11)

解决方案:滚动自己的符合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)
  }
}

我很想听听你最终使用的内容。

UPDATE:事实证明 OP写了我引用的代码(Swift 4.0更新到Cache)来解决问题。当然,这个代码应该在这里,但我也会把我的文字保留为未经编辑,因为它具有戏剧性的讽刺意味。 :)

答案 1 :(得分:9)

您可以使用非常优雅的解决方案,对KeyedDecodingContainerKeyedEncodingContainer类进行扩展:

enum ImageEncodingQuality: CGFloat {
    case png = 0
    case jpegLow = 0.2
    case jpegMid = 0.5
    case jpegHigh = 0.75
}

extension KeyedEncodingContainer {

    mutating func encode(_ value: UIImage,
                         forKey key: KeyedEncodingContainer.Key,
                         quality: ImageEncodingQuality = .png) throws {
        var imageData: Data!
        if quality == .png {
            imageData = UIImagePNGRepresentation(value)
        } else {
            imageData = UIImageJPEGRepresentation(value, quality.rawValue)
        }
        try encode(imageData, forKey: key)
    }

}

extension KeyedDecodingContainer {

    public 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 SDKError.imageConversionError
        }
    }

}

这是一个用法示例:

class DocumentScan: Codable {

    private enum CodingKeys: String, CodingKey {
        case scanDate
        case image
    }

    let scanDate: Date
    let image: UIImage

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        scanDate = try container.decode(Date.self, forKey: .scanDate)
        image = try container.decode(UIImage.self, forKey: .image)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(scanDate, forKey: .scanDate)
        try container.encode(image, forKey: .image, quality: .png)
    }
}

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

答案 2 :(得分:4)

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

将UIImage转换为func encode(to encoder: Encoder) throws内的字符串:

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

将String转换回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)

答案 3 :(得分:2)

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

您必须对图像 scale 及其 pngData() 进行编码。然后当您解码 UIImage 时,通过调用 init(data:scale:) 将数据与比例组合。

答案 4 :(得分:0)

最简单的方法就是将其设置为Data而不是UIImage

public struct SomeImage: Codable {

    public let photo: Data

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

反序列化:

UIImage(data: instanceOfSomeImage.photo)!

答案 5 :(得分:0)

还有一个在图像上使用惰性变量的简单解决方案:

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

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

由于 UIImage 初始化占用大量资源,我们将它们结合在一起。 只需注意整个初始化将在后台线程上进行。