Recently, I began work on a new iOS app and had to work with Codable
, the new protocol introduced in Swift 4 that makes converting between Swift types and JSON data incredibly easy. However, Codable has some limitations and issues I had to work around.
While by default Codable supports most common Swift types, including strings, arrays, number types and more, out of the box, it does not support some platform specific types. For example, if you try to use a UIColor or UIImage, you'll get an error telling you that your Type does not conform to protocol 'Decodable'
.
In this blog post, I'll explain how to use UIImage or UIColor together with Codable, so that you can load a custom theme or images from your server.
Getting started
If you're reading this blog post, I'm going to assume you are already familiar with using Codable and how it works. If you aren't, I'd strongly recommend that you go and read Paul's great blog post about it, then come back to this.
Here is a short example, with an array of Blog objects in JSON and a corresponding Swift struct:
[{
"name": "Julian Schiavo's Blog",
"blogAddress": "https://schiavo.me"
}]
struct Blog: Codable {
var name: String
var blogAddress: URL
}
The above example could be decoded like this:
let decoder = JSONDecoder()
let decodedBlogs = try decoder.decode([Blog].self, from: data)
print(decodedBlogs) // [__lldb_expr_2.Blog(name: "Julian Schiavo's Blog", blogAddress: https://schiavo.me)]
⚠️ Note: If your JSON is inside a String, you need to convert it to a
Data
object like this:Data(string.utf8)
because theJSONDecoder
expects Data.
Colors
Codable is great for easy JSON conversion without the need for third parties or complicated custom decoding code, but if we try to have a variable containing a UIColor, we'll receive a strange error:
struct Person: Codable { // Type 'Person' does not conform to protocol 'Decodable'
var name: String
var favoriteColor: UIColor
}
By default, Codable automatically adds support for JSON encoding and decoding when we conform to it. However, since UIColor
does not implement the Decodable
(or Encodable
) protocol, Codable isn't able to let our object be encoded/decoded, because it would fail to support one of the properties.
Although it isn't supported automatically, there are a few ways to resolve this, the best one being using a wrapper class like this:
struct Color: Codable {
var red: CGFloat
var green: CGFloat
var blue: CGFloat
var alpha: CGFloat?
var uiColor: UIColor {
return UIColor(red: red, green: green, blue: blue, alpha: alpha ?? 1)
}
}
We can then use the uiColor
computed variable for labels, controls, and other things which need a UIColor.
An example JSON to use together with our wrapper class would look something like this:
[{
"color": {
"red": 18,
"green": 26,
"blue": 47
}
}]
If you wanted to, you could add support for hex colors to UIColor with a custom init, then use those in your JSON to make it shorter and simpler.
Images
As with colors, Codable also doesn't support decoding or encoding UIImages. Although we could do something similar to what we did with colors and just use the image data, it would be better if we supported loading images from a URL, because JSONs aren't great to store any type of raw data.
First, we'll add a custom extension to UIImage
to allow us to load images from a URL. UIImage already supports loading from data and we can load data from a URL, but by using a custom initializer like this we can do all the hard work of using threads and safely loading the image in one place. We'll also use the new Result type in Swift 5 for a cleaner implementation.
extension UIImage {
/// Loads an image from the specified URL on a background thread
static func load(from url: URL, completion: @escaping (Result<UIImage, NetworkError>) -> Void) {
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: url)
guard let image = UIImage(data: data) else {
completion(.failure(.failedToLoad))
return
}
DispatchQueue.main.async {
completion(.success(image))
}
} catch {
completion(.failure(.failedToLoad))
}
}
}
}
Next, we can create a special UIImage wrapper, like the one we used before for UIColor:
class Image: Codable {
var uiImage: UIImage?
init(image: UIImage? = nil) {
self.uiImage = image
}
}
However, this will give us the same error as we used to get without the wrapper class! Because we want to support loading images from URLs, we'll have to use a custom Codable implementation.
To support URLs as well as later on encoding into images (so we can cache them to disk), we'll need a custom CodingKeys
enum (we're explicitly setting the image
case's string value to uiImage
to match our variable's name):
enum CodingKeys: String, CodingKey {
case image = "uiImage"
case url
}
Now, we can add the custom decoder implementation:
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let imageURL = try? values.decode(URL.self, forKey: .url) {
UIImage.load(from: imageURL) { (result) in
switch result {
case .success(let image):
self.uiImage = image
case .failure(let error):
// Handle the error or throw it to make the Decoder fail
}
}
} else if let imageData = try? values.decode(Data.self, forKey: .image),
let image = UIImage(data: imageData) {
self.uiImage = image
}
}
We're just telling the decoder we want to use our custom CodingKeys. Then, we ask it to decode a URL from the JSON and use it to load and set the image.
As you can see, we also support decoding from image data. This is so that we can also support encoding our custom wrapper class and saving it to disk, with a custom encoder like this:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let image = uiImage {
let encodedImage = image.jpegData(compressionQuality: 1)
try container.encode(encodedImage, forKey: .image)
}
}
However, do note that this is not recommended for most apps as it is usually best to save images as files and load them from their file URL.
Conclusion
In this tutorial, we explored how to add support for Codable to 2 types that don't normally support it, so you can now use images or colors in your JSON with ease. This technique can be applied to any Swift type that doesn't support Codable, by using the same kind of wrapper class and format.
I hope this tutorial helped you out if you were trying to use colors or images in your JSON, or taught you a new trick you might find useful in the future. If you have any questions or feedback, feel free to send them or email me. Also, make sure to subscribe to my weekly newsletter below so you don't miss a single post! Thanks for reading 😊