41

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?

onmyway133
  • 45,645
  • 31
  • 257
  • 263
  • 3
    What if you create a `sub class` of `UIImage` that confirms to `Codable` and add required initializer on that. – Torongo Sep 13 '17 at 12:49
  • 1
    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. – Hamish Sep 13 '17 at 14:52
  • 1
    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 string – Leo Dabus Sep 13 '17 at 16:13
  • 1
    @Hamish @LeoDabus I don't mention json or xml in my question. I think you suggest `JSONEncoder`? but it is just one implementation of `Encoder` protocol – onmyway133 Sep 13 '17 at 17:08
  • 1
    @onmyway133 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`. – Hamish Sep 13 '17 at 17:18

8 Answers8

42

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)!
J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • looks so good!! – Malwinder Singh Aug 19 '22 at 09:29
  • 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 instead – Peter Lapisu Aug 22 '22 at 08:55
  • 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 approach – Peter Lapisu Aug 23 '22 at 09:32
33

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. :)

AmitaiB
  • 1,656
  • 1
  • 19
  • 19
  • 13
    thanks. Did you know I implemented that Please see the commits – onmyway133 Sep 21 '17 at 05:23
  • 2
    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? – AmitaiB Sep 24 '17 at 17:31
  • 1
    Hi, I would like to see if there's any clever solution than mine – onmyway133 Sep 25 '17 at 09:00
  • 2
    FWIW, I might suggest throwing `DecodingError.dataCorruptedError(forKey:in:debugDescription:)`, etc., instead. – Rob May 01 '20 at 21:39
  • 1
    Thanks for introducing this wrapper, work like a charm. I'd like to see how encoder/decoder's container works in this context. – Zhou Haibo Dec 22 '20 at 11:56
  • 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. – matt Jun 27 '21 at 21:40
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

Vitalii Gozhenko
  • 9,220
  • 2
  • 48
  • 66
  • 2
    This is a great answer! +1 I would however change the ImageEncodingQuality enum to ```enum ImageType { case png; case jpeg(CGFloat) } ``` – freytag Jan 09 '20 at 11:03
  • @freytag you are reading my mind, I already switched to custom enum for `ImageType` in my projects :-) – Vitalii Gozhenko Jan 09 '20 at 14:03
  • 3
    Pretty good! But you should use `DecodingError` and `EncodingError`. –  Jun 23 '20 at 20:08
  • 1
    i would advise against storing as jpg... in that case each save lowers the quality, even the image didn't change... – Peter Lapisu Aug 22 '22 at 09:02
  • 1
    @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 is – Vitalii Gozhenko Aug 25 '22 at 16:20
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)
oddRaven
  • 672
  • 1
  • 7
  • 20
  • 4
    `Data` implements `Codable`, so you don't need to convert to a String ;) https://developer.apple.com/documentation/foundation/data/2895337-encode – Axel Guilmin Jun 29 '19 at 22:23
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:).

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    See https://stackoverflow.com/a/68137443/341994 for an actual example of a codable image wrapper that passes the equality test. – matt Jun 27 '21 at 21:42
  • 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://stackoverflow.com/a/66588264/1271826. – Rob Jul 20 '22 at 22:21
  • Thanks, that really saves me some debugging time. – Wizard Nov 05 '22 at 21:07
5

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
        }
    }
    
}
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
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.

OhadM
  • 4,687
  • 1
  • 47
  • 57
  • be cautious, this can lead to data and image not represent each other... eg. mainImageData changes, but the mainImage still points to the old image – Peter Lapisu Aug 22 '22 at 08:58
  • Thanks @PeterLapisu, this is just an example, they're a lot more edge cases. – OhadM Aug 27 '22 at 11:07
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
        }
    }
}
MaatheusGois
  • 139
  • 5