Updating movies in the background

We have almost all of the building blocks required to update movies in the background in place. All we need now is a way to fetch movies from the movie database using their remote ID instead of using the movie database search API.

In order to enable this way of querying movies, another fetch method is required. The simplest way to do this would be to copy and paste both the fetch and URL building methods and adjust them to enable fetching movies by ID. This isn't the best idea; if we add another fetch method or require more flexibility later, we will be in trouble. It's much better to refactor this into a more flexible format right away.

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 MovieDBHelperstruct:

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:

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) 
} 
 
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 will require 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 fetch method that qualify for this are as follows:

  • 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 JSON key, where the popularity and ID reside. 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 shouldn't involve more than retrieving a URL that may or may not be valid and providing a blueprint to retrieve the data we're looking for in the JSON response, and we should be able to kick off the request. Based on this, we can write the following code:

typealias JSON = AnyObject 
typealias IdAndRating = (id: Int?, rating: Double?) 
typealias DataExtractionCallback = (JSON) ->IdAndRating 
 
private func fetchRating(fromUrlurl: URL?, extractData: 
  DataExtractionCallback, callback: 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, 
            let json = try? JSONSerialization.jsonObject(with: 
              data, options: []) 
            else { return } 
 
        let resultingData = extractData(json) 
        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 (AnyObject) -> (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, 
    let json = try? JSONSerialization.jsonObject(with: data, options: []) 
    else { return } 
 
let resultingData = extractData(json) 
rating = resultingData.rating 
remoteId = resultingData.id 

What's interesting here is that regardless of what we're doing, we will need to extract the json 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: MovieDBCallback) { 
    let searchUrl = url(forMovie: movie) 
    let extractData: DataExtractionCallback = { json in 
        guard let results = json["results"] as? [[String:AnyObject]], 
            let popularity = results[0]["popularity"] as? Double, 
            let id = results[0]["id"] as? Int 
            else { return (nil, nil) } 
 
        return (id, popularity) 
    } 
 
    fetchRating(fromUrl: searchUrl, extractData: extractData, callback: callback) 
} 
 
func fetchRating(forMovieId id: Int64, callback: MovieDBCallback) { 
    let movieUrl = url(forMovieId: id) 
    let extractData: DataExtractionCallback = { json in 
        guard let popularity = json["popularity"] as? Double, 
            let id = json["id"] as? Int 
            else { return (nil, nil) } 
 
        return (id, 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 json 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 actually write code that updates our movies. Let's go!

Updating the movies

The process of updating movies is a strange process. As we saw earlier, network requests are performed asynchronously, which means that you can't rely on the network request being finished by the time a function is finished executing. Because of this, a callback is used, which enables us to know when a request is done.

But what happens if we need to wait for multiple requests? How do we know that we have finished making all of the requests to update movies? Since the movie database doesn't allow us to fetch all of our movies at once, we'll need to make a bunch of requests. When all of these requests are complete, we'll need to invoke the background fetch completionHandler with the result of our operation.

To achieve this, we will need to make use of grand central dispatch. More specifically, we will use a dispatch group. A dispatch group keeps track of an arbitrary number of tasks, and it won't consider itself as completed until all of the tasks that are added to the group have finished executing.

This behavior is exactly what we need. Whenever we fetch a movie from the network, we can add a new task that we'll complete once the underlying movie is updated. Finally, when all of the movies are updated we can report back to the completionHandler to inform it about our results. Let's take a step-by-step look at how to achieve this behavior using the following code:

func application(_ application: UIApplication, performFetchWithCompletionHandler 
  completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 
 
    let fetchRequest: NSFetchRequest<Movie> = Movie.fetchRequest() 
    let managedObjectContext = persistentContainer.viewContext 
    guard let allMovies = try? managedObjectContext.fetch(fetchRequest) else { 
        completionHandler(.failed) 
        return 
    }  
} 

This first part of the implementation is fairly straightforward. We obtain a fetch request and a managed object context. Then, we query the managed object context for all of its movies, and if we are unable to fetch any, we'll notify the completion handler that we failed to fetch a new data and we exit the method.

All the following code snippets should be added to the application(_:performFetchWithCompletionHandler:) method inside of AppDelegate in the same order as they are presented. A full overview of the implementation will be provided at the end:

let queue = DispatchQueue(label: "movieDBQueue") 
let group = DispatchGroup() 
let helper = MovieDBHelper() 
var dataChanged = false 

These lines create a dispatch queue and a dispatch group. We'll use the dispatch queue as a mechanism to execute the tasks inside of our group. We'll also create an instance of our helper struct, and a variable is created to keep track of whether we received updates to our data or not:

for movie in allMovies { 
    queue.async(group: group) { 
        group.enter() 
        helper.fetchRating(forMovieId: movie.remoteId) { id, popularity in 
            guard let popularity = popularity, 
                popularity != movie.popularity else { 
                group.leave() 
                return 
            } 
             
            dataChanged = true 
             
            managedObjectContext.persist { 
                movie.popularity = popularity 
                group.leave() 
            } 
        } 
    } 
     
} 

Next, we loop through the fetched movies. We add the code we want to execute to the dispatch queue and call group.enter(). This informs the dispatch group that we just added a task to it. Then, we perform our fetch for the rating and check whether or not a popularity was fetched and whether it's different from the one we currently have on the movie. If either of these isn't the case, we call group.leave() to tell the group this task is complete and return from the callback.

If our guard passes, we are dealing with updated data, so we set dataChanged to true. We also persist the new popularity to CoreData and then leave the group. Once again, the persist method is very convenient because it makes sure that we're executing the code inside of the persist block on the right thread.

The following final snippet we will need to add will execute when all the tasks in the queue are performed; at this point, we would want to check whether we've fetched new data by reading the dataChanged property, and based on this property, we will call the callbackHandler:

group.notify(queue: DispatchQueue.main) { 
    if dataChanged { 
        completionHandler(.newData) 
    } else { 
        completionHandler(.noData) 
    } 
} 

The group.notify method takes a queue and a block of code that we want to execute. The queue is set to the main queue, which means that the code inside of the block executed on the main queue. Then, we read the dataChanged variable and inform the completionHandler about the result of the fetch operation.

As promised, the full implementation for application(_:performFetchWithCompletionHandler:) is as follows:

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 
         
    let fetchRequest: NSFetchRequest<Movie> = Movie.fetchRequest() 
    let managedObjectContext = persistentContainer.viewContext 
    guard let allMovies = try? managedObjectContext.fetch(fetchRequest) else { 
        completionHandler(.failed) 
        return 
    } 
         
    let queue = DispatchQueue(label: "movieDBQueue") 
    let group = DispatchGroup() 
    let helper = MovieDBHelper() 
    var dataChanged = false 
     
    for movie in allMovies { 
        queue.async(group: group) { 
            group.enter() 
            helper.fetchRating(forMovieId: movie.remoteId) { id, popularity in 
                guard let popularity = popularity, 
                    popularity != movie.popularity else { 
                    group.leave() 
                    return 
                } 
                 
                dataChanged = true 
                 
                managedObjectContext.persist { 
                    movie.popularity = popularity 
                    group.leave() 
                } 
            } 
        } 
    } 
     
    group.notify(queue: DispatchQueue.main) { 
        if dataChanged { 
            completionHandler(.newData) 
        } else { 
            completionHandler(.noData) 
        } 
    } 
} 
..................Content has been hidden....................

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