Swift

How To Fetch Data From An API In Swift

Introduction

Fetching JSON data from a remote API really is the bread and butter of iOS development so it’s important for you to know how to do it in a neat and reusable way. Today, we will be using Codable and URLSession. Oh, and some generics too. We will be using the website https://jsonplaceholder.typicode.com/todos. This handily provides us with some example JSON data that we can use.

[
  {
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
  },
  {
    "userId": 1,
    "id": 2,
    "title": "quis ut nam facilis et officia qui",
    "completed": false
  }
]

Defining Our Model

To get started, we first need to define a model for the data. We will be using codable for this which allows us to easily parse the JSON data from the API into a swift struct.

struct ToDo: Decodable {
  let userId: Int
  let id: Int
  let title: String
  let completed: Bool
}

There are a few things to notice here. Firstly, our struct conforms to Decodable and not Codable . This might be a bit confusing at first but Codable is actually just a typealias for both Encodable and Decodable. Encodable is reponsible for encoding a struct into JSON data and, you probably guessed, Decodable is for decoding JSON data into a struct. For our example, we are only fetching data so all we need to do is decode it.

Secondly, you can see that the property names match the exact keys used in the JSON data. It’s vital these match otherwise swift won’t be able to decode the data. But what happens when the JSON keys aren’t suitable for swift code? Luckily we can address this. In this example, I don’t particularly like the completed name and would prefer isComplete instead. To allow this to work, we need to provide an enum of coding keys like so:

struct ToDo: Decodable {
  let userId: Int
  let id: Int
  let title: String
  let isComplete: Bool
  
  enum CodingKeys: String, CodingKey {
    case isComplete = "completed"
    case userId, id, title
  }
}

Fetching The Data

Once we’ve defined our model, we can finally talk to our API. We’ll be using URLSession and to keep our code nice and clean, we can use an extension. I like to put this in a new file but feel free to put it wherever you want.

extension URLSession {
  func fetchData(at url: URL, completion: @escaping (Result<[ToDo], Error>) -> Void) {
    self.dataTask(with: url) { (data, response, error) in
      if let error = error {
        completion(.failure(error))
      }

      if let data = data {
        do {
          let toDos = try JSONDecoder().decode([ToDo].self, from: data)
          completion(.success(toDos))
        } catch let decoderError {
          completion(.failure(decoderError))
        }
      }
    }.resume()
  }
}

This method uses some of my favourite parts of swift. Firstly the result type to return our data. We can use this to pass either the data back or any errors we encounter. Next we have the actual data task that fetches any data from a given URL. Once we get that data, we use a JSON decoder to convert it into our custom ToDo type (thanks Codable).

To use this shiny new method we’ve written, it’s dead easy.

let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
URLSession.shared.fetchData(at: url) { result in
  switch result {
  case .success(let toDos):
    // Woo, we got our todo list
  case .failure(let error):
    // Ohno, an error, let's handle it
  }
}

Some Nice Improvements

Our method works well but what if we need to handle a new type of data? We would need to create another method to handle this. There must be another way… This is where generics come in! We can rewrite our method to not care about the type of data it handles, as long as the type conforms to Decodable, it will fetch the data and decode it.

extension URLSession {
  func fetchData<T: Decodable>(for url: URL, completion: @escaping (Result<T, Error>) -> Void) {
    self.dataTask(with: url) { (data, response, error) in
      if let error = error {
        completion(.failure(error))
      }

      if let data = data {
        do {
          let object = try JSONDecoder().decode(T.self, from: data)
          completion(.success(object))
        } catch let decoderError {
          completion(.failure(decoderError))
        }
      }
    }.resume()
  }
}

The method is incredible similar but we’ve said we want to use a generic object T that conforms to decodable (the bit in the angle brackets at the start of the method). We then just replace every use of [ToDo] with T . The generic syntax can seem a bit weird at first but if you follow it through slowly, it does start to make sense.

To use our rewritten method it is basically the same, we just need to specify the type we want:

let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
  URLSession.shared.fetchData(for: url) { (result: Result<[ToDo], Error>) in
    switch result {
    case .success(let toDos):
      // A list of todos!
    case .failure(let error):
      // A failure, please handle
  }
}

Conclusion

I hope this article helped you out. It’s always good to try and future-proof code as much as possible and to make it reusable and readable.

Thanks for reading!