Preparing the helper struct

To maintain a clear overview of the available API endpoints, you can add a nested enum to MovieDBHelper. Doing this will make other parts of the code more readable, help to avoid errors, and abstract away code duplication. An associated value will be used on the enum to hold onto the ID of a movie. This is quite convenient because the movie ID is part of the API endpoint.

Add the following code inside of the MovieDBHelper struct:

static let apiKey = "d9103bb7a17c9edde4471a317d298d7e"

enum Endpoint {
  case search
  case movieById(Int64)

  var urlString: String {
    let baseUrl = "https://api.themoviedb.org/3/"

    switch self {
    case .search:
      var urlString = "(baseUrl)search/movie/"
      urlString = urlString.appending("?api_key=(MovieDBHelper.apiKey)")
      return urlString
    case let .movieById(movieId):
      var urlString = "(baseUrl)movie/(movieId)"
      urlString = urlString.appending("?api_key=(MovieDBHelper.apiKey)")
      return urlString
    }
  }
}

Note that the apiKey constant has been changed from an instance property to a static property. Making it a static property makes it available to be used inside of the nested Endpoint enum. Note that the value associated with the movieById case is Int64 instead of Int. This is required because the movie ID is a 64-bit integer type in Core Data.

With this new Endpoint enum in place, you can refactor the way you build the URLs as follows:

private func url(forMovie movie: String) -> URL? {
  guard let query = movie.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
    else { return nil }

  var urlString = Endpoint.search.urlString
  urlString = urlString.appending("&query=(query)")

  return URL(string: urlString)
}

private func url(forMovieId id: Int64) -> URL? {
  let urlString = Endpoint.movieById(id).urlString
  return URL(string: urlString)
} 

The url(forMovie:) method was updated to make use of the Endpoint enum. The url(forMovieId:) method is new and uses the Endpoint enum to obtain a movie-specific URL easily.

When you fetch data, there are some pieces of code that you will have to write regardless of the URL that will ultimately be used to fetch data. When you look at the fetchRating(forMovie:) method, there are a couple of things that apply to both endpoints you will eventually use to retrieve movie information. The following list is an overview of these things:

  • Checking whether we're working with a valid URL
  • Creating the data task
  • Extracting the JSON
  • Calling the callback

If you think about it, the only real difference is the API response object that is used. When you examine the JSON responses returned by the search API and fetching a movie by ID, the difference is that the search API returns an array of movies where you're interested in the first result. The movie-by-id API returns the correct movie as part of the root object.

With this in mind, the refactored code should be able to retrieve the desired data using just a URL, a data-extraction strategy, and a callback. Based on this, you can write the following code:

// 1
typealias IdAndRating = (id: Int?, rating: Double?)
typealias DataExtractionStrategy = (Data) -> IdAndRating

// 2
private func fetchRating(fromUrl url: URL?, withExtractionStrategy extractionStrategy: @escaping DataExtractionStrategy, callback: @escaping MovieDBCallback) {
  guard let url = url else {
    callback(nil, nil)
    return
  }

  let task = URLSession.shared.dataTask(with: url) { data, response, error in
    var rating: Double? = nil
    var remoteId: Int? = nil

    defer {
      callback(remoteId, rating)
    }

    guard error == nil
      else { return }

    guard let data = data
      else { return }

    // 3  
    let extractedData = extractionStrategy(data)
    rating = extractedData.rating
    remoteId = extractedData.id
  }

  task.resume()
}

There is quite a lot going on in the preceding snippet. Most of the code will look familiar, but some of the details might be new. Let's go over the comments in this code one by one:

  1. This part defines a couple of type aliases that will make the code a little bit more readable. The first alias is for a named tuple that contains the movie's ID and rating. The second alias defines the signature for the data extraction closure.
  2. The second highlight is for fetchRating(fromUrl:withExtractionStrategy:callback:). This method will be used to obtain movie data. It's marked private because it's not very useful to call this method directly. Instead, it will be called by two methods that will be implemented soon.
  3. The data for the movie is extracted from the raw data by passing the raw data to the data extraction closure.

Let's use this method to implement both the old way of fetching a movie through the search API and the new way that uses the movie ID to request the resource directly, as follows:

func fetchRating(forMovie movie: String, callback: @escaping MovieDBCallback) {
  let searchUrl = url(forMovie: movie)
  let extractData: DataExtractionStrategy = { data in
    let decoder = JSONDecoder()

    guard let response = try? decoder.decode(MovieDBLookupResponse.self, from: data),
      let movie = response.results.first
      else { return (nil, nil) }

    return (movie.id, movie.popularity)
  }

  fetchRating(fromUrl: searchUrl, withExtractionStrategy: extractData, callback: callback)
}

func fetchRating(forMovieId id: Int64, callback: @escaping MovieDBCallback) {
  let movieUrl = url(forMovieId: id)
  let extractData: DataExtractionStrategy = { data in
    let decoder = JSONDecoder()

    guard let movie = try? decoder.decode(MovieDBLookupResponse.MovieDBMovie.self, from: data)
      else { return (nil, nil) }

    return (movie.id, movie.popularity)
  }

  fetchRating(fromUrl: movieUrl, withExtractionStrategy: extractData, callback: callback)
}

The code duplication is minimal in these methods, which means that this attempt at refactoring the code was a huge success. If you add new ways to fetch movies, all you will need to do is obtain a URL, define how to retrieve the data you're looking for from the data object, and finally, kick off the fetching.

You're now finally able to fetch movies using their ID without duplicating a lot of code. The final step in implementing the background update feature is to implement the code that updates movies. Let's go!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset