How to conform UIImage to Codable?
Asked Answered
F

8

43

Swift 4 has Codable and it's awesome. But UIImage does not conform to it by default. How can we do that?

I tried with singleValueContainer and unkeyedContainer

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

I get 2 errors

  1. 'required' initializer must be declared directly in class 'UIImage' (not in an extension)
  2. A non-failable initializer cannot delegate to failable initializer 'init(data:)' written with 'init?'

A workaround is to use wrapper. But are there any other ways?

Failsafe answered 13/9, 2017 at 12:36 Comment(5)
What if you create a sub class of UIImage that confirms to Codable and add required initializer on that.Milepost
Why exactly do you want to conform UIImage to Codable? Images generally aren't good candidates for being encoded to formats such as JSON or XML. Usually it's better to encode the image separately, and then encode for example a URL in the JSON.Lindie
If you need to save your image inside a JSON string just convert your image data to a base64 string and save it as a stringReich
@Lindie @LeoDabus I don't mention json or xml in my question. I think you suggest JSONEncoder? but it is just one implementation of Encoder protocolFailsafe
@Failsafe My main question was just asking why you wanted this :) The rest was an assumption based on the current (and commonly used) encoders/decoders now provided by Foundation.Lindie
B
35

A solution: roll your own wrapper class conforming to Codable.

One solution, since extensions to UIImage are out, is to wrap the image in a new class you own. Otherwise, your attempt is basically straight on. I saw this done beautifully in a caching framework by Hyper Interactive called, well, Cache.

Though you'll need to visit the library to drill down into the dependencies, you can get the idea from looking at their ImageWrapper class, which is built to be used like so:

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

Here is their wrapper class:

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

I'd love to hear what you end up using.

UPDATE: It turns out the OP wrote the code that I referenced (the Swift 4.0 update to Cache) to solve the problem. The code deserves to be up here, of course, but I'll also leave my words unedited for the dramatic irony of it all. :)

Blockade answered 20/9, 2017 at 23:27 Comment(6)
thanks. Did you know I implemented that 😉 Please see the commitsFailsafe
Ah, well I certainly know now! 😂 It hadn't occurred to me that the world could be that small. Lesson learned. Hmmm, doesn't that also mean that 'my' answer is the Accepted one, hmmm?Blockade
Hi, I would like to see if there's any clever solution than mineFailsafe
FWIW, I might suggest throwing DecodingError.dataCorruptedError(forKey:in:debugDescription:), etc., instead.Dualpurpose
Thanks for introducing this wrapper, work like a charm. I'd like to see how encoder/decoder's container works in this context.Amargo
I believe that this is wrong. A UIImage is more than just its data. In particular, by throwing away the scale information, you can end up deserializing a very different image from what you serialized.Inaptitude
A
43

Properly the easiest way is to just make the property Data instead of UIImage like this:

public struct SomeImage: Codable {

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

Deserialize the image:

UIImage(data: instanceOfSomeImage.photo)!
Altruistic answered 17/3, 2019 at 19:27 Comment(3)
looks so good!!Only
in general this is not a good approach as you need to make sure the two properties Data and corresponding UIImage are in sync... like you can end up with data that doesn't mach the image and vice versa... also using this on more properties adds quite a lot of boilerplate... see the property wrapper approach insteadComplicate
ok, than it lacks the support for the property to update, so if you would change it to var, you run into the issue i described above... this is simply a too basic approachComplicate
B
35

A solution: roll your own wrapper class conforming to Codable.

One solution, since extensions to UIImage are out, is to wrap the image in a new class you own. Otherwise, your attempt is basically straight on. I saw this done beautifully in a caching framework by Hyper Interactive called, well, Cache.

Though you'll need to visit the library to drill down into the dependencies, you can get the idea from looking at their ImageWrapper class, which is built to be used like so:

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

Here is their wrapper class:

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

I'd love to hear what you end up using.

UPDATE: It turns out the OP wrote the code that I referenced (the Swift 4.0 update to Cache) to solve the problem. The code deserves to be up here, of course, but I'll also leave my words unedited for the dramatic irony of it all. :)

Blockade answered 20/9, 2017 at 23:27 Comment(6)
thanks. Did you know I implemented that 😉 Please see the commitsFailsafe
Ah, well I certainly know now! 😂 It hadn't occurred to me that the world could be that small. Lesson learned. Hmmm, doesn't that also mean that 'my' answer is the Accepted one, hmmm?Blockade
Hi, I would like to see if there's any clever solution than mineFailsafe
FWIW, I might suggest throwing DecodingError.dataCorruptedError(forKey:in:debugDescription:), etc., instead.Dualpurpose
Thanks for introducing this wrapper, work like a charm. I'd like to see how encoder/decoder's container works in this context.Amargo
I believe that this is wrong. A UIImage is more than just its data. In particular, by throwing away the scale information, you can end up deserializing a very different image from what you serialized.Inaptitude
W
20

You can use very elegant solution using extension for KeyedDecodingContainer and KeyedEncodingContainer classes:

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: You can use such way to adopt Codable to any class type

Westbound answered 28/6, 2018 at 18:21 Comment(5)
This is a great answer! +1 I would however change the ImageEncodingQuality enum to enum ImageType { case png; case jpeg(CGFloat) } Homology
@Homology you are reading my mind, I already switched to custom enum for ImageType in my projects :-)Westbound
Pretty good! But you should use DecodingError and EncodingError.Manual
i would advise against storing as jpg... in that case each save lowers the quality, even the image didn't change...Complicate
@PeterLapisu I completely agree with you, that's why there is 2 options: PNG and JPEG. JPEG may be useful when you want for some reason to transfer it through network once and quality is not critical for you, but saving traffic isWestbound
G
7

One way to pass an UIImage is to convert it to something that conforms to Codable, like String.

To convert the UIImage to String inside func encode(to encoder: Encoder) throws:

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

To convert the String back to UIImage inside required init(from decoder: Decoder) throws:

let strBase64: String = try values.decode(String.self, forKey: .image)
let dataDecoded: Data = Data(base64Encoded: strBase64, options: .ignoreUnknownCharacters)!
image = UIImage(data: dataDecoded)
Germanophobe answered 1/3, 2018 at 8:12 Comment(1)
Data implements Codable, so you don't need to convert to a String ;) developer.apple.com/documentation/foundation/data/…Harkey
C
6

The best solution is to use a custom property wrapper

  • the property remains mutable
  • no code alternations needed, just add the @CodableImage prefix

Usage

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

Add this code to project:

@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
        }
    }
    
}
Complicate answered 25/7, 2022 at 12:50 Comment(0)
I
5

The existing answers all appear to be incorrect. If you compare the deserialized image with the original, you will find they may well not be equal in any sense. This is because the answers are all throwing away the scale information.

You have to encode the image scale as well as its pngData(). Then when you decode the UIImage, combine the data with the scale by calling init(data:scale:).

Inaptitude answered 27/6, 2021 at 21:42 Comment(2)
See https://mcmap.net/q/390297/-uiimage-not-equivalent-when-encoding-decoding for an actual example of a codable image wrapper that passes the equality test.Inaptitude
You should also be careful about the UIImage rotation data (especially if the images were taken with the iOS camera). pngData doesn't capture that. E.g. https://mcmap.net/q/390298/-uiimage-upside-down-when-reconverted-from-string-stored-in-sqlite.Dualpurpose
K
0

There's also a simple solution using lazy var on the image:

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

This way, during object initialization and assignment to mainImageData, its didSet will kick in which will then initiate the initialization of the UIImage.

Since UIImage initialization is resource heavy, we couple them together. Just pay attention that the entire initialization will be on the background thread.

Kithara answered 5/5, 2021 at 7:46 Comment(2)
be cautious, this can lead to data and image not represent each other... eg. mainImageData changes, but the mainImage still points to the old imageComplicate
Thanks @PeterLapisu, this is just an example, they're a lot more edge cases.Kithara
L
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
        }
    }
}
Lappet answered 27/7, 2022 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.