Implementing the fetch logic

The asynchronous nature of network requests makes certain tasks, such as the one we're about to implement, quite complex. Usually, when you write code, its execution is very predictable. Your app typically runs line by line, so any line that comes after the previous one can assume that the previous line has finished executing. This isn't the case with asynchronous code. Asynchronous code is taken off the main thread and runs separately from the rest of your code. This means that your asynchronous code might run in parallel with other code. In the case of a network request, the asynchronous code might execute seconds after the function in which the request started.

This means that you need to figure out a way to update and save the movie as soon as its data has been retrieved. What's interesting about this is that once you see the code that implements this feature, it will feel natural to you that this is how it works. However, it's important that you're aware of the fact that it's not as straightforward as it may seem at first.

It's also important that you're aware of the fact that the code we will look at in a moment is executed across threads. This means that even though all pieces of the code are defined in the same place, they are not executed on the same thread. The callback for the network request is executed on a different thread than the code that initiated the network request. We discussed earlier that Core Data is not thread-safe. This means that you can't safely access a Core Data object on a different thread other than the thread it was created on.

If this confuses you, that's okay. You're supposed to be a bit confused right now. Asynchronous programming is not easy, and fooling you into thinking it is will cause you frustration once you run into concurrency related troubles (and you will). Whenever you work with callbacks, closures, and multiple threads, you should be aware that you're doing complex work that isn't straightforward.

Now that we've established an understanding about the complexity of the asynchronous code, let's take a more concrete look at what we're dealing with. It's time to start implementing the network request that fetched popularity ratings for movies. We will abstract the fetching logic into a helper named MovieDBHelper. Go ahead and create a new helper folder in Xcode and add a new Swift file called MovieDBHelper.swift to it.

Abstracting this logic into a helper has multiple advantages. One of them is simplicity; it will keep our view controller code nice and clean. Another advantage is flexibility. Let's say that you want to combine multiple rating websites, or a different API, or compute popularity based on the number of family members who added this same title to their list; it will be easier to implement since all logic for ratings is in a single place.

Add the following skeleton implementation to the MovieDBHelper file:

import Foundation 

struct MovieDBHelper {
typealias MovieDBCallback = (Double?) -> Void
let apiKey = "YOUR_API_KEY_HERE"

func fetchRating(forMovie movie: String, callback: @escaping MovieDBCallback) {

}

private func url(forMovie movie: String) -> URL? {
var urlString = "https://api.themoviedb.org/3/search/movie/"
urlString = urlString.appending("?api_key=\(apiKey)")
urlString = urlString.appending("&query=\(movie)")

return URL(string: urlString)
}
}

The preceding code starts off with an interesting line:

typealias MovieDBCallback = (Double?) -> Void 

This line specifies the type we will use for the callback closure that's called when the rating is fetched. This means that our view controller is only concerned with whether a rating is found. If it can't find a rating, the Double? will be nil. If it did find a rating, this argument will contain the rating. This supplements the flexibility mentioned earlier.

Next, we have a dummy method that performs the fetch; we will implement this method soon. Finally, we have a method that builds a URL. This method is private because it's only supposed to be used inside of the helper struct. Before we implement fetchRating(forMovie:callback), add a new file named MovieDBResponse.swift to the helper folder. We will use this file to define a struct that represents the response we expect to receive from the Moviedb API. Add the following implementation to this file:

struct MovieDBLookupResponse: Codable {

struct MovieDBMovie: Codable {
let popularity: Double?
}

let results: [MovieDBMovie]
}

The preceding code uses a nested struct to represent the movie objects that we're interested in. This is similar to what you have already seen earlier, in the playground example. Structuring the response this way makes it very obvious what our intent is, which usually makes code easier to reason with. With this struct in place, let's see what the implementation of fetchRating(forMovie:callback) looks like in the following code:

func fetchRating(forMovie movie: String, callback: @escaping MovieDBCallback) { 
guard let searchUrl = url(forMovie: movie) else {
callback(nil)
return
}

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

defer {
callback(rating)
}

let decoder = JSONDecoder()

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

rating = popularity
}

task.resume()
}

This implementation looks very similar to what we experimented with earlier in our playground. The URL building method is used to create a valid URL. If this fails, we can't continue the retrieval of the rating, so the callback is called with a nil argument. This will inform the caller of this method that the execution is done and we didn't find a result.

Next, a new data task is created, as shown before, and resume is called on this task to kick it off. There is an interesting aspect of how the callback for this data task is called, though. Let's take a look at the following lines of code:

var rating: Double? = nil 

defer {
callback(rating)
}

A rating double is created here, and it is given an initial value of nil. Then there's a defer block. The code inside of the defer block is called right before exiting the scope. In other words, it's executed right before we return from a function or closure.

Since this defer block is defined inside the callback for the data task, the callback for the fetchRating(forMovie:callback:) method is always called just before we exit the data task callback. This is convenient because all we must do is set the value for our rating to a double, and we don't have to manually invoke the callback for each possible way we exit the scope. This also applies when we're returning because of unmet requirements. For instance, if there is an error while calling the API, we don't need to invoke the callback. We can simply return from the closure, and the callback is called automatically. This strategy can also be applied if you instantiate or configure objects temporarily and you want to perform some clean-up when the method, function, or closure is done.

The rest of the code should be fairly straightforward since most of it is nearly identical to the code used in the playground. Now that we have the networking logic down, let's take a look at how to actually update our movie object with a popularity rating.