Implementing the fetch logic

The asynchronous nature of network requests makes certain tasks, such as the one you'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 line before it 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 that initiated the request.

This means that you need to figure out a way to update and save movies that were added as soon as the rating 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 you're about to look at is executed on multiple 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. You have already learned that Core Data is not thread-safe. This means that you can't safely access a Core Data object on a different thread 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 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 you understand that asynchronous code is hard, let's take a closer look at the feature you're about to implement. It's time to start implementing the network request that fetches popularity ratings for movies. You 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 the logic for ratings is in a single place.

Add the following skeleton implementation to the MovieDBHelper file:

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? {
    guard let query = movie.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
      else { return nil }

    var urlString = "https://api.themoviedb.org/3/search/movie/"
    urlString = urlString.appending("?api_key=(apiKey)")
    urlString = urlString.appending("&query=(query)")

    return URL(string: urlString)
  }
}

The preceding code starts off with an interesting line:

typealias MovieDBCallback = (Double?) -> Void

This line specifies the type that's used for the callback closure that's called when the rating is fetched. This callback will receive an optional Double as its argument. If the network request fails for any reason, the Double will be nil. Otherwise, it contains the rating for the movie that the request was created for.

The snippet also contains a dummy method that performs the fetch; you will implement this method soon. Finally, there's a method that builds a URL. This method is private because it's only supposed to be used inside of the helper struct. Note that the movie is converted to a percent-encoded string. This is required because if your user adds a movie with spaces in it, you would end up with an invalid URL if the spaces aren't properly encoded.

Before you implement fetchRating(forMovie:callback), add a new file named MovieDBResponse.swift to the helper folder. This file will be used 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 are part of the response. This is similar to what you saw in the playground example. Structuring the response this way makes the intent of this helper very obvious, which usually makes code easier to reason about. 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 you experimented with earlier in the playground. The URL-building method is used to create a valid URL. If this fails, it makes no sense to attempt requesting the movie's rating, so the callback is called with a nil argument. This will inform the caller of this method that the execution is done and no result was retrieved.

Next, a new data task is created and resume is called on this task to kick it off. There is an interesting aspect to 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 the code returns 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 the data task callback is exited. This is convenient because all you must do is set the value for the rating to a double, and you don't have to manually invoke the callback for each possible way the scope can be exited. This also applies when you return because of unmet requirements. For instance, if there is an error while calling the API, you don't need to invoke the callback. You 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 you have the networking logic down, let's take a look at how to actually update the movie object with a popularity rating.

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

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