Cleaning up Decodable Responses with Enums in Swift
Tags: ios, swift
Published: 2020-08-10 11:20:46

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.
```json
  "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:
```ruby
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.
```swift
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`.
```swift
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.

```swift
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 enums `content` and `action`, both of which use associated values.
- I have improved the naming of properties e.g. `Content` instead of `ContentType`. 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:
```swift
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](https://twitter.com/keke_arif).

Cheers,

Keke