How to conform UIImage to Codable?
Solution 1
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. :)
Solution 2
Properly the easiest way is to just make it Data
instead of UIImage
:
public struct SomeImage: Codable {
public let photo: Data
public init(photo: UIImage) {
self.photo = photo.pngData()!
}
}
Deserialize the image:
UIImage(data: instanceOfSomeImage.photo)!
Solution 3
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
Solution 4
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)
Solution 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:)
.
onmyway133
I work with iOS, macOS, Android, React, Electron and Nodejs. I actively work on open source with 1.3k+ followers on GitHub, 45k+ apps touched and 3.4m+ downloads on CocoaPods. I also write on Medium with 2.3k+ followers with 90k+ monthly views. Support my apps https://onmyway133.com/apps Open source https://github.com/onmyway133 Writing https://medium.com/@onmyway133
Updated on June 13, 2022Comments
-
onmyway133 about 2 years
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
andunkeyedContainer
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
- 'required' initializer must be declared directly in class 'UIImage' (not in an extension)
- 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 almost 7 yearsthanks. Did you know I implemented that 😉 Please see the commits
-
AmitaiB almost 7 yearsAh, 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?
-
onmyway133 almost 7 yearsHi, I would like to see if there's any clever solution than mine
-
Axel Guilmin about 5 years
Data
implementsCodable
, so you don't need to convert to a String ;) developer.apple.com/documentation/foundation/data/… -
freytag over 4 yearsThis is a great answer! +1 I would however change the ImageEncodingQuality enum to
enum ImageType { case png; case jpeg(CGFloat) }
-
Vitalii Gozhenko over 4 years@freytag you are reading my mind, I already switched to custom enum for
ImageType
in my projects :-) -
Rob about 4 yearsFWIW, I might suggest throwing
DecodingError.dataCorruptedError(forKey:in:debugDescription:)
, etc., instead. -
Jessy about 4 yearsPretty good! But you should use
DecodingError
andEncodingError
. -
ChuckZHB over 3 yearsThanks for introducing this wrapper, work like a charm. I'd like to see how encoder/decoder's container works in this context.
-
matt about 3 yearsI 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 about 3 yearsSee stackoverflow.com/a/68137443/341994 for an actual example of a codable image wrapper that passes the equality test.