Let's have a quick discussion about how to clean up your response objects using enums with associated values. I am assuming you are usingSwift's Decodable
protocol for parsing data. If you're not using it and still stuck using a 3rd-party then it's time to make the change. Don't be a dinosaur. Let's take a look at an example API response.
"posts": [
{
"id": 1,
"content_type": "text",
"description": "Hello world",
"image_url": null,
"cta_type": "url",
"cta_url": "goole.com",
"cta_file_url": null
}
]
cta_type
is defined on the backend as:
enum content_type: {
text: 0,
image: 1,
}
enum cta_type: {
no_action: 0,
url: 1,
deep_link: 2,
file: 3,
}
This response is describing a post object in our app. It can be either a text or image. It can also have an optional call-to-action button that either opens a url, uses a deep link or opens a file (PDF). First lets check out the most obvious struct implementation.
struct Post: Decodable {
private enum CodingKeys: String, CodingKey {
case id
case contentType = "content_type"
case desc = "description"
case imageURL = "image_url"
case ctaType = "cta_type"
case ctaFileURL = "cta_file_url"
}
enum ContentType: String, Decodable {
case text, image
}
enum CTAType: String, Decodable {
case url, file
case noAction = "no_action"
case deepLink = "deep_link"
}
let id: Int
let contentType: ContentType
let desc: String?
let imageURL: String?
let ctaType: CTAType
let ctaURL: String?
let ctaFileURL: String?
}
We get the benefit of the free init(from decoder: Decoder)
method from the above implementation however there is a glaring problem with this response object. Consider configuring the UI by using switch
on contentType
.
switch contentType {
case .text:
guard let desc = desc else { return }
setupTextView(with: desc)
case .image:
guard let imageURL = imageURL else { return }
setupImageView(with: imageURL)
}
After switching the enum we have to try and access the properties for desc
and imageURL
separately, this makes the enum pretty redundant. A much more swifty way of dealing with this type of data would be to use enums with
associated values. We can make this magic happen by implementing our own init(from decoder: Decoder)
method. Below is the cleaned up version.
struct Post: Decodable {
// MARK: - Coding Keys
private enum CodingKeys: String, CodingKey {
case id
case content = "content_type"
case desc = "description"
case imageURL = "image_url"
case action = "cta_type"
case ctaURL = "cta_url"
case ctaFileURL = "cta_file_url"
}
private enum ContentKeys: String, Decodable {
case text, image
}
private enum ActionKeys: String, Decodable {
case url, file
case none = "no_action"
case deepLink = "deep_link"
}
// MARK: - Properties
enum Content {
case text(String), image(URL)
}
enum Action {
case none, url(URL), deepLink(String)
}
let id: Int
let content: Content
let action: Action
// MARK: - Decodable
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let content = try container.decode(ContentKeys.self, forKey: .content)
let desc = try container.decode(String.self, forKey: .desc)
let imageURL = try container.decode(String.self, forKey: .imageURL)
let action = try container.decode(ActionKeys.self, forKey: .action)
let ctaURL = try container.decode(String.self, forKey: .ctaURL)
let ctaFileURL = try container.decode(String.self, forKey: .ctaFileURL)
id = try container.decode(Int.self, forKey: .id)
order = try container.decode(Int.self, forKey: .order)
switch content {
case .text:
self.content = .text(desc)
case .image:
if let imageURL = URL(string: imageURL) {
self.content = .image(imageURL)
} else {
throw someCustomError()
}
}
switch action {
case .none: self.action = .none
case .deepLink: self.action = .deepLink(ctaURL)
case .file:
if let ctaFileURL = URL(string: ctaFileURL) {
self.action = .file(ctaFileURL)
} else {
throw someCustomError()
}
case .url:
if let ctaURL = URL(string: ctaURL) {
self.action = .file(ctaURL)
} else {
throw someCustomError()
}
}
}
There are a couple of improvements:
- I am now using three very concise properties
id
and the enumscontent
andaction
, both of which use associated values. - I have improved the naming of properties e.g.
Content
instead ofContentType
. It's no necessary to follow the backend's naming scheme if something seems unclear so remove ambiguity at the client level.
Note that I also throw a custom error if decoding fails and I declared two new enums ContentKeys
and ActionKeys
to help decode the raw part of the response.
It's now much easier to use the data:
switch contentType {
case .text(let desc):
setupTextView(with: desc)
case .image(let imageURL):
setupImageView(with: imageURL)
}
Thanks for reading. Hope this helps you when writing response objects. If you enjoyed the post give me a follow on Twitter @keke_arif.
Cheers,
Keke
Top comments (0)