Updating movies in the background

The final step in enabling background fetch for our application is to add the application(_:performFetchWithCompletionHandler:) method. As explained before, this method will be called by iOS whenever we're awoken from the background and it allows us to perform an arbitrary amount of work. Once we're done performing our task, we must call the completion handler that iOS has passed in to this method.

Upon calling the completion handler, we will inform iOS about the results of our operation. It's important to correctly report this status because background fetch is intended to improve the user experience. If you falsely report to iOS that you have new data all the time so your app is woken up more often, you're actually degrading the user experience. You should trust the system to judge when your app is woken up. It's in the best interest of your users, their battery life, and ultimately your app to not abuse background fetch.

In order to efficiently implement background fetch, we will take the following steps:

  1. Updating the data model so we can query the movie database more efficiently.
  2. Refactoring the existing code to use the improved data model.
  3. Implementing background fetch with the existing helper struct.

The first two steps are not directly tied to implementing background fetch, but they do illustrate that an efficient background fetch strategy may involve refactoring some of your app's existing code. Remember, there is nothing wrong with refactoring old code to implement a new feature. Both the new feature and the old code will benefit from refactoring your app.

Updating the data model

The data model we currently have associates movies with a single family member. This means that we could potentially store the same movie over and over again. When we were only storing data, this wasn't that big of a deal. However, now that we will query the movie database in a background fetch task, we need this task to be as fast as we possibly can make it. This means that we don't want to ask for the same movie twice. Also, we most certainly don't want to use the search API as we did before, we want to be as specific about a movie as we can.

To facilitate this, we will change the relationship between movies and family members to a many-to-many relationship. We'll also add a new field to the movie entity: remoteId. This remoteId will hold the identifier the movie database uses for the particular movie so we can use it directly in later API calls.

Open the model editor in Xcode and add the new property to Movie. Make sure that it's a 64-bit integer and that it's optional. Also, select the familyMember relationship and change it to a To Many relationship in the sidebar. It's also a good idea to rename the relationship to familyMembers since we're now relating it to more than one family member.

Updating the data model

Great, the model has been updated. We still need to perform a bit of work though. Because we changed the name and nature of the family member relationship, our code won't compile. Make the following modifications to the managedObjectContextDidChange(_:) method in MoviesViewController.swift; the modified lines are highlighted:

if let updatedObjects = userInfo[NSUpdatedObjectsKey] as? Set<Movie> { 
    for object in updatedObjects { 
        if let familyMember = self.familyMember, 
            let familyMembers = object.familyMembers 
            where, familyMembers.contains(familyMember) { 
 
            tableView.reloadData() 
            break 
        } 
    } 
} 

There is just one more model-related change that we will need to incorporate. In order to efficiently search for an existing movie or create a new one, we will add an extension to the Movie model. Create a new group called Models and add a new Swift file named Movie.swift to it. Finally, add the following implementation to the file:

import CoreData 
 
extension Movie { 
    static func find(byName name: String, orCreateIn moc: 
      NSManagedObjectContext) -> Movie { 
        let predicate = Predicate(format: "name ==[dc] %@", name) 
        let request: NSFetchRequest<Movie> = Movie.fetchRequest() 
        request.predicate = predicate 
 
        guard let result = try? moc.fetch(request) 
            else { return Movie(context: moc) } 
 
        return result.first ?? Movie(context: moc) 
    } 
} 

The preceding code will query CoreData for an existing movie with the same name. This matching will be done case insensitively because people might write the same movie name with different capitalization. If we aren't able to find a result, or if the results come back empty, we will create a new movie. Otherwise, we return the first and presumably the only result CoreData has for our query. This wraps up the changes we need to make to our data layer.

Refactoring the existing code

Our existing code compiles, but it's not optimal yet. The MovieDBHelper doesn't pass the movie's remote ID to its callback, and the movie insertion code doesn't use this remote ID yet. When the user wants to save a new movie, the app still defaults to creating a new movie even though we just wrote our helper method to either find or create a movie to avoid data duplication. We should update our code so the callback is called with the fetched remote ID.

Let's update the MovieDBHelper first. Replace the following lines in the fetchRating(forMovie:callback:) method; changes are highlighted:

typealias MovieDBCallback = (Int?, Double?) -> Void 
let apiKey = "YOUR_API_KEY_HERE" 
 
func fetchRating(forMovie movie: String, callback: MovieDBCallback) { 
    guard let searchUrl = url(forMovie: movie) else { 
        callback(nil, nil) 
        return 
    } 
 
    let task = URLSession.shared().dataTask(with: searchUrl) 
      { 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: []), 
            let results = json["results"] as? [[String:AnyObject]], 
            let popularity = results[0]["popularity"] as? Double, 
            let id = results[0]["id"] as? Int 
            else { return } 
 
        rating = popularity 
    remoteId = id 
    } 
 
     task.resume() 
} 

These updates change the callback handler so it takes both the remote ID and the rating as parameters. We also add a variable to hold the remote ID, and we incorporate this variable into the callback. We also extract the ID from our JSON object.

With this code, the MovieDBHelper is fully up to date. Let's update the movie creation code to wrap up the refactoring step. Update the following lines in the MoviesViewController'ssaveMovie(withName:) method; changes are once again highlighted:

moc.persist { 
    let movie = Movie.find(byName: name, orCreateIn: moc) 
    if movie.name == nil || movie.name?.isEmpty == true { 
        movie.name = name 
    } 
 
    familyMember.favoriteMovies = familyMember.favoriteMovies?.adding(movie) 
 
    let helper = MovieDBHelper() 
    helper.fetchRating(forMovie: name) { remoteId, rating in 
        guard let rating = rating, 
            let remoteId = remoteId 
            else { return } 
 
        moc.persist { 
            movie.popularity = rating 
            movie.remoteId = remoteId 
        } 
    } 
} 

First, the preceding code either fetches an existing movie or creates a new one with the find(byName:orCreateIn:) method we just created. Next, it checks whether or not the returned movie already has a name. If it doesn't have a name yet, we will set it. Also, if it does have a name, we can safely assume we were handed an existing movie object so we don't need to set the name. Next, the rating and ID are fetched and we set the corresponding properties on the movie object to the correct values in the callback.

This is all the code we needed to refactor to prepare our app for background fetch. Let's implement the actual updating feature now.

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

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