Preparing the helper struct

In order to maintain a clear overview of the available API endpoints, we will add a nested enum to the MovieDBHelper. Doing this will make other parts of our code more readable, and we can avoid errors and abstract away duplication with this enum. We'll make use of an associated value on the enum to hold on to the ID of a movie; this is convenient because the movie ID is part of the API endpoint.

Add the following code inside of the MovieDBHelper struct:

static let apiKey = "YOUR_API_KEY_HERE" 

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

The line that defines the apiKey constant is highlighted because it's been changed from an instance property to a static property. Making it a static property enables us to use it inside of the nested Endpoint enum. Note that the value associated with the movieById case in the switch is Int64 instead of Int. This is required because the movie ID is a 64-bit integer type in CoreData.

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

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

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

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 easily obtain a movie-specific URL.

Fetching the rating without writing a lot of duplicate code requires us to abstract away all of the code that we will have to write regardless of the URL we will use to fetch the movie data. The parts of the current fetch method that qualify for this are as follows:

If you think about it, the only real difference is the API response that is used. In the search results, this information is stored in the first item inside of a result's array. In the single movie API call, it's inside of the root object.

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

typealias IdAndRating = (id: Int?, rating: Double?) 
typealias DataExtractionCallback = (Data) -> IdAndRating

private func fetchRating(fromUrl url: URL?, extractData: @escaping DataExtractionCallback, 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 }

let resultingData = extractData(data)
rating = resultingData.rating
remoteId = resultingData.id
}

task.resume()
}

There is quite a lot going on in the preceding snippet. Most of the code will look familiar, but the type aliases created at the beginning of the code might throw you off a bit. These aliases are intended to make our code a bit more readable. After all, DataExtractionCallback is much easier to read than (Data) -> (id: Int?, rating: Double?). Whenever you create a callback or a tuple, it's often a good idea to use a typealias. This will improve your code's readability tremendously.

The following section in the fetchRating(fromUrl:extractData:callback:) method is where the DataExtractionCallback is used:

guard let data = data
else { return }

let resultingData = extractData(data)
rating = resultingData.rating
remoteId = resultingData.id

What's interesting here is that regardless of what we're doing, we will need to extract the data object. This object is then passed to the extractData closure, which returns a tuple containing the data we're interested in.

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: DataExtractionCallback = { 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, extractData: extractData, callback: callback)
}

func fetchRating(forMovieId id: Int64, callback: @escaping MovieDBCallback) {
let movieUrl = url(forMovieId: id)
let extractData: DataExtractionCallback = { 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, extractData: extractData, callback: callback)
}

The code duplication is minimal in these methods, which means that this refactor action is a success. If we add new ways to fetch movies, all we need to do is obtain a URL, explain how to retrieve the data we're looking for from the data object, and finally, we need to kick off the fetching.

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