Updating the movies

The process of updating movies is a strange one. As you 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. The callback is then called when the network request is done.

But what happens if you need to wait for multiple requests? How do you know that all requests to update movies have been completed? Since the movie database doesn't allow developers to fetch multiple movies at once, a bunch of requests must be made. When all of these requests are complete, the background fetch completionHandler should be called with the result of the operation.

To achieve this, you can make use of the grand central dispatch. More specifically, you can 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 precisely what's needed to wait for all movies to be updated. Whenever we fetch a movie from the network, you can add a new task to the dispatch group that will be marked as completed once the underlying movie is updated. Finally, when all of the movies are updated, completionHandler can be called to inform iOS about the result of the background fetch. Let's take a step-by-step look at how to achieve this behavior. Start by adding the following code to AppDelegate:

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 background fetch is relatively straightforward. All it does is retrieve all movie objects from the database. If this fails, completionHandler is called with a .failed status.

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. The dispatch queue represents the background thread on which the fetch operations will be executed. Next, add the following snippet:

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

This part of the implementation loops through the fetched movies. For every movie, a task is added to the dispatch queue and group.enter() is called to tell the dispatch group that a new task has just been added. The next step is to fetch the rating. If this fails, group.leave() is called to tell the dispatch group that this task has finished. If the data was retrieved successfully, the movie is updated with the fetched rating. Once the managed object has persisted the changes, group.leave() is called, and the operation has finished.

The next and final snippet that must be added executes when all the tasks in the queue are performed; at this point, the code should check whether new data has been fetched by reading the dataChanged property, and based on this property, call 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 it should execute. The queue is set to the main queue, which means that the code inside of the block is performed on the main queue. Then, the dataChanged variable is read, and completionHandler is called accordingly.

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

To test whether background fetching is working as expected, you can build and run your app. Then, add a new movie, so you have a movie for which the ID is stored. Finally, you can use the debug menu item in Xcode that's at the top of your screen to simulate a background refresh. This will trigger a background refresh.