DEV Community

Cover image for Cleaning up Decodable Responses with Enums in Swift
Keke Arif
Keke Arif

Posted on

Cleaning up Decodable Responses with Enums in Swift

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

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

Discussion (0)