Episode #387

Creating an API Client

Series: Making a Podcast App From Scratch

18 minutes
Published on April 18, 2019

This video is only available to subscribers. Get access to this video and 572 others.

It's time to start talking to external APIs to get the data we want to display in the app. We start by exploring the API we want to consume with Paw, a useful macOS app. We then create a simple API client class that abstracts most of the boilerplate logic around how to handle the various URLSession outcomes.

Episode Links

In this episode, we will create an API Client to fetch the top rated podcasts from iTunes. We will use this as suggestions on the search screen.

RSS Feed Generator

Apple provides RSS Feed Generator for top podcasts, where we can set the genre, result limit, format and options for allowing explicit content. This generates a Feed URL we can use to fetch the list.

Apple Podcast Feed Generator Website

Creating our API Client

We’ll start by creating a class: TopPodcastsAPI.

class TopPodcastsAPI {
    private let session: URLSession
    private let baseURL = URL(string: "https://rss.itunes.apple.com/api/v1/us/podcasts/")!

    init(session: URLSession = .shared) {
        self.session = session
    }
}

To pull the feed data we are initializing with URLSession and baseURL. Note that URLSession is initialized as shared.

Creating Function to Fetch Podcasts

Start by defining the function fetchTopPodcasts. Let’s define the limit as 50 and allowExplicit as false. It’s always good to have these parameters as configurable.

func fetchTopPodcasts(limit: Int = 50, allowExplicit: Bool = false, completion: @escaping (Result<Data, APIError>) -> Void) {
let explicit = allowExplicit ? "explicit" : "non-explicit"
  let path = "top-podcasts/all/\(limit)/\(explicit).json"
  let url = baseURL.appendingPathComponent(path)
  let request = URLRequest(url: url)
  perform(request: request, completion: completion)
}

Define the completion handler as @escaping so the the provided block can capture the scope and return after the calling function is executed. Note that the completionHandler is void. Swift 5's Result type takes the Data (success type) which represents the response obtained from the feed and Error (failure type).

Create an enum to represent errors

Create an enum APIError outside the class. It defines multiple types of HTTP errors.

enum APIError : Error {
    case networkingError(Error)
    case serverError // HTTP 5xx
    case requestError(Int, String) // HTTP 4xx
    case invalidResponse
}

We'll use these errors to represent the multiple possible failure paths for a network request.

Create a simple URLRequest by appending path and baseURL.

Creating a reusable method to perform network requests

This calls the session.dataTask method which will retrieve feed / data from URL request object and calls the completion handler.

let task = session.dataTask(with: request) { data, response, error in
if let error = error {
    DispatchQueue.main.async {
        completion(.failure(.networkingError(error)))
    }
    return
   }
}

Note that completionHandler is called on the main queue. We need to parse the response in JSON format which takes time, so we will do this on a background queue first, then jump over to the main queue to call the completionHandler.

Make sure to handle the other errors requestError and serverError using http.statusCode.

guard let http = response as? HTTPURLResponse, let data = data else {
    DispatchQueue.main.async {
        completion(.failure(.invalidResponse))
      }
    return
}

Verify HTTP status code of 200 and call the completion block with the data.

switch httpResponse.status {
case 200:
  DispatchQueue.main.async {
  completion(.success(data))
}

Don't forget to start the request by executing resume on the task:

task.resume()

This episode uses Xcode 10.2, Swift 5.0.