Updating Core Data objects with fetched data

So far, the only thing we stored in Core Data is movie names. We will expand this functionality by performing a lookup for a certain movie name through the movie database API. We will use the fetched information to display and store a popularity rating for the movies in our database.

A task such as this seems straightforward at first; you could come up with a flow as the one shown in the following steps:

  1. The users fill out their favorite movie.
  2. It fetches popularity.
  3. It stores the movie and its popularity.
  4. The interface updates with the new movie.

At first sight, this is a fine strategy; insert the data when you have it. However, it's important to consider that API calls are typically done asynchronously so the interface stays responsive. Also, more importantly, API calls can be really slow if your user doesn't have a good Internet connection. This means that you would be updating the interface with a noticeable lag if the preceding steps are executed one by one.

The following would be a much better approach to do this:

  1. The users fill out their favorite movie.
  2. Store the movie.
  3. Update the interface with the new movie.
  4. Begin popularity fetching.
  5. Update the movie in the database.
  6. Update the interface with popularity.

This approach is somewhat more complex, but it will give the user a very snappy and responsive experience. The interface will respond to new movies immediately, and we will update the interface whenever we fetch a new data. Before we can fetch the data and update our models, we will update our Core Data model.

Open up the model editor and select the Movie entity. All you have to do is add a new property and name it popularity. Select the Double type for this property because the popularity is stored as a decimal value. You have to make sure that this property is optional since you won't be able to provide a value for it straight away:

Updating Core Data objects with fetched data

If you've worked with Core Data pre iOS 10, this is the part where you expect to read about migrations and how you can orchestrate them. However, this isn't needed anymore in iOS 10. All you need to do now is simply build and run your application to regenerate your model definitions, and for a simple change, such as the one we performed just now, Core Data will automatically manage the migration on its own.

Note

If you want to support iOS versions below 10, make sure that you read up on Core Data migrations. Whenever you update your models you have to make sure that your database can properly migrate from one model version to another. During development, this isn't extremely important; you just reinstall the app whenever your models change. However, app updates will crash on launch if the Core Data model isn't compatible with the previous model.

Now that the model is updated, we can figure out how to implement the flow described earlier.

Implementing the fetch logic

The asynchronous nature of network requests makes tasks, such as the one we're about to perform, quite complex. When you're writing your code, you often think of it linearly. Your app will run 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 we will need to figure out a way for us to update and save the movie as soon as we have the data required. What's interesting about this is that once you see the required code, 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 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 down the line. 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 asynchronous code, let's take a more concrete look at what we're actually dealing with. It's time to start implementing our network request. 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 rating logic are 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 or not a rating is found. If we can't find a rating, the Double? will be nil. If we did find a rating, this argument will contain the rating. This aids the flexibility mentioned earlier.

Next, we have a dummy method that will perform the actual fetch in a bit and 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.

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) 
        } 
         
        guard error == nil 
            else { return } 
         
        guard let data = data, 
            let jsonObject = try? JSONSerialization.jsonObject(with: 
              data, options: []), 
            let json = jsonObject as? [String: AnyObject], 
            let results = json["results"] as? [[String:AnyObject]], 
            let popularity = results[0]["popularity"] as? Double 
            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, we will create a new data task as shown before and call resume to kick it off. There is an interesting aspect to the callback for this data task though. Let's take a look at the following lines of code:

var rating: Double? = nil 
 
defer { 
    callback(rating) 
} 

We will create a rating double, assign it nil, and 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 actually return from a function or closure.

Since this defer block is defined inside of 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 have to do is set the value for our rating to a double and we don't have to manually invoke the callback. This also applies when we're returning due to 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.

Updating a movie with a popularity rating

To update our movie, we will implement the final step of the approach that was outlined earlier. We need to asynchronously fetch a rating from the movie database and then we use that rating to update the movie. The following code should be added right after adding a new movie to a family member, inside of the persist block, approximately at line 45 (depending on your own formatting):

let helper = MovieDBHelper() 
helper.fetchRating(forMovie: name) { rating in 
    guard let rating = rating 
        else { return } 
 
    moc.persist { 
        movie.popularity = rating 
    } 
} 

You can see that our abstraction provides a nice interface to our view controller. We can simply use the helper and provide it a movie to fetch the rating for with a callback and we're all set. Abstracting code away like this can make maintaining your code a lot more fun in the long run.

The most surprising thing in the preceding snippet is that we're calling moc.persist again inside of the callback. We will need to do this because this callback is actually executed after the initial persist has long finished. Actually, this callback isn't even executed on the same thread as the code it's surrounded by. Now that we saw all the code involved, let's see what all this threading talk means in a more visual way.

Visualizing multiple threads

The following figure will help us understand multiple threads:

Visualizing multiple threads

When we call saveMovie(withName:), we're still on the main thread. We open up the persistence block, create the movie, set its name, create the helper, and then call fetchRating(forMovie:callback:) on the helper. This call itself is still on the main thread. However, the actual fetching of data is pushed to a background thread. This was discussed earlier when you experimented with fetching data in a playground.

The callback that's invoked by our dataTask is called on the same thread as the task itself is fetching data on: the background thread. We will do what we need to do with the JSON and finally call the callback that was passed to fetchRating(forMovie:callback:). The code inside of this callback is pulled into the background thread and executed there.

You can see that the set movie rating step in the update flow is somehow pushed back on the main thread. This is due to the persist method we added as an extension to the managed object context. The context uses the perform method internally to ensure that any code we execute inside of the persist block is executed on the thread the managed object context is on. Also, since we created the managed object context on the main thread, the movie rating will be set on the main thread.

Note

If we didn't set the movie rating on the same thread as the managed object belongs to, we'd get errors and undefined behavior. Always make sure that you manipulate Core Data objects on the same thread as their managed object context.

Threading is a complex subject but it's essential for building responsive applications. Network logic is a great example of why multithreading is important. If we didn't perform the networking on a separate thread, the interface would be unresponsive for the duration of the request. If you have other operations that might take a while in your app, consider moving them on to a background thread so they don't block the user interface.

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

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