Core Data and Swift: Asynchronous Fetching

In the previous installments, we discussed batch updates and batch deletes. In this tutorial, we'll take a closer look at how to implement asynchronous fetching and in what situations your application can benefit from this new API.

1. The Problem

Like batch updates, asynchronous fetching has been on the wish list of many developers for quite some time. Fetch requests can be complex, taking a non-trivial amount of time to complete. During that time the fetch request blocks the thread it's running on and, as a result, blocks access to the managed object context executing the fetch request. The problem is simple to understand, but what does Apple's solution look like.

2. The Solution

Apple's answer to this problem is asynchronous fetching. An asynchronous fetch request runs in the background. This means that it doesn't block other tasks while it's being executed, such as updating the user interface on the main thread.

Asynchronous fetching also sports two other convenient features, progress reporting and cancellation. An asynchronous fetch request can be cancelled at any time, for example, when the user decides the fetch request takes too long to complete. Progress reporting is a useful addition to show the user the current state of the fetch request.

Asynchronous fetching is a flexible API. Not only is it possible to cancel an asynchronous fetch request, it's also possible to make changes to the managed object context while the asynchronous fetch request is being executed. In other words, the user can continue to use your application while the application executes an asynchronous fetch request in the background.

3. How Does It Work?

Like batch updates, asynchronous fetch requests are handed to the managed object context as an NSPersistentStoreRequest object, an instance of the NSAsynchronousFetchRequest class to be precise.

An NSAsynchronousFetchRequest instance is initialized with an NSFetchRequest object and a completion block. The completion block is executed when the asynchronous fetch request has completed its fetch request.

Let's revisit the to-do application we created earlier in this series and replace the current implementation of the NSFetchedResultsController class with an asynchronous fetch request.

Step 1: Project Setup

Download or clone the project from GitHub and open it in Xcode 7. Before we can start working with the NSAsynchronousFetchRequest class, we need to make some changes. We won't be able to use the NSFetchedResultsController class for managing the table view's data since the NSFetchedResultsController class was designed to run on the main thread.

Step 2: Replacing the Fetched Results Controller

Start by updating the ViewController class as shown below. We remove the fetchedResultsController property and create a new property,items, of type [Item] for storing the to-do items. This also means that the ViewController class no longer needs to conform to the NSFetchedResultsControllerDelegate protocol.

Before we refactor the viewDidLoad() method, I first want to update the implementation of the UITableViewDataSource protocol. Take a look at the changes I've made.

We also need to change one line of code in the prepareForSegue(_:sender:) method as shown below.

Last but not least, delete the implementation of the NSFetchedResultsControllerDelegate protocol since we no longer need it.

Step 3: Creating the Asynchronous Fetch Request

As you can see below, we create the asynchronous fetch request in the view controller's viewDidLoad() method. Let's take a moment to see what's going on.

We start by creating and configuring an NSFetchRequest instance to initialize the asynchronous fetch request. It's this fetch request that the asynchronous fetch request will execute in the background.

To initialize an NSAsynchronousFetchRequest instance, we invoke init(request:completionBlock:), passing in fetchRequest and a completion block.

The completion block is invoked when the asynchronous fetch request has completed executing its fetch request. The completion block takes one argument of type NSAsynchronousFetchResult, which contains the result of the query as well as a reference to the original asynchronous fetch request.

In the completion block, we invoke processAsynchronousFetchResult(_:), passing in the NSAsynchronousFetchResult object. We'll take a look at this helper method in a few moments.

Executing the asynchronous fetch request is almost identical to how we execute an NSBatchUpdateRequest. We call executeRequest(_:) on the managed object context, passing in the asynchronous fetch request.

Even though the asynchronous fetch request is executed in the background, note that the executeRequest(_:) method returns immediately, handing us an NSAsynchronousFetchResult object. Once the asynchronous fetch request completes, that same NSAsynchronousFetchResult object is populated with the result of the fetch request.

Remember from the previous tutorial that executeRequest(_:) is a throwing method. We catch any errors in the catch clause of the do-catch statement and print them to the console for debugging.

Step 4: Processing the Asynchronous Fetch Result

The processAsynchronousFetchResult(_:) method is nothing more than a helper method in which we process the result of the asynchronous fetch request. We set the view controller's items property with the contents of the result's finalResult property and reload the table view.

Step 5: Build & Run

Build the project and run the application in the iOS Simulator. If your application crashes when it attempts to execute the asynchronous fetch request, then you may be using an API that is deprecated as of iOS 9 (and OS X El Capitan).

In Core Data and Swift: Concurrency, I explained the different concurrency types a managed object context can have. As of iOS 9 (and OS X El Capitan), the ConfinementConcurrencyType is deprecated. The same is true for the init() method of the NSManagedObjectContext class, because it creates an instance with a concurrency type of ConfinementConcurrencyType.

If your application crashes, you are most likely using a managed object context with a ConfinementConcurrencyType concurrency type, which doesn't support asynchronous fetching. Fortunately, the solution is simple. Create a managed object context using the designated initializer, init(concurrencyType:), passing in MainQueueConcurrencyType or PrivateQueueConcurrencyType as the concurrency type.

4. Showing Progress

The NSAsynchronousFetchRequest class adds support for monitoring the progress of the fetch request and it's even possible to cancel an asynchronous fetch request, for example, if the user decides that it's taking too long to complete.

The NSAsynchronousFetchRequest class leverages the NSProgress class for progress reporting as well as canceling an asynchronous fetch request. The NSProgress class, available since iOS 7 and OS X Mavericks, is a clever way to monitor the progress of a task without the need to tightly couple the task to the user interface.

The NSProgress class also support cancelation, which is how an asynchronous fetch request can be canceled. Let's find out what we need to do to implement progress reporting for the asynchronous fetch request.

Step 1: Adding SVProgressHUD

We'll show the user the progress of the asynchronous fetch request using Sam Vermette's SVProgressHUD library. The easiest way to accomplish this is through CocoaPods. This is what the project's Podfile looks like.

Run pod install form the command line and don't forget to open the workspace CocoaPods has created for you instead of the Xcode project.

Step 2: Setting Up NSProgress 

In this article, we won't explore the NSProgress class in much detail, but feel free to read more about it in Apple's documentation. We create an NSProgress instance in the view controller's viewDidLoad() method, before we execute the asynchronous fetch request.

You may be surprised that we set the total unit count to 1. The reason is simple. When Core Data executes the asynchronous fetch request, it doesn't know how many records it will find in the persistent store. This also means that we won't be able to show the relative progress to the user—a percentage. Instead, we will show the user the absolute progress—the number of records it has found.

You could remedy this issue by performing a fetch request to fetch the number of records before you execute the asynchronous fetch request. I prefer not to do this though, because this also means that fetching the records from the persistent store takes longer to complete because of the extra fetch request at the start.

Step 3: Adding an Observer

When we execute the asynchronous fetch request, we are immediately handed an NSAsynchronousFetchResult object. This object has a progress property, which is of type NSProgress. It's this progress property that we need to observe if we want to receive progress updates.

Note that we call resignCurrent on the progress object to balance the earlier becomeCurrentWithPendingUnitCount: call. Keep in mind that both of these methods need to be invoked on the same thread.

Step 4: Removing the Observer

In the completion block of the asynchronous fetch request, we remove the observer and dismiss the progress HUD.

Before we implement observeValueForKeyPath(_:ofObject:change:context:), we need to show the progress HUD before creating the asynchronous fetch request.

Step 5: Progress Reporting

All that's left for us to do, is implement the observeValueForKeyPath(_:ofObject:change:context:) method. We check if context is equal to ProgressContext, create a status object by extracting the number of completed records from the change dictionary, and update the progress HUD. Note that we update the user interface on the main thread.

5. Dummy Data

If we want to properly test our application, we need more data. While I don't recommend using the following approach in a production application, it's a quick and easy way to populate the database with data.

Open AppDelegate.swift and update the application(_:didFinishLaunchingWithOptions:) method as shown below. The populateDatabase() method is a simple helper method in which we add dummy data to the database.

The implementation is straightforward. Because we only want to insert dummy data once, we check the user defaults database for the key "didPopulateDatabase". If the key isn't set, we insert dummy data.

The number of records is important. If you plan to run the application on the iOS Simulator, then it's fine to insert 100,000 or 1,000,000 records. This won't work as good on a physical device and will take too long to complete.

In the for loop, we create a managed object and populate it with data. Note that we don't save the changes of the managed object context during each iteration of the for loop.

Finally, we update the user defaults database to make sure the database isn't populated the next time the application is launched.

Great. Run the application in the iOS Simulator to see the result. You'll notice that it takes a few moments for the asynchronous fetch request to start fetching records and update the progress HUD.

Showing The Progress of The Asynchronous Fetch Request

6. Breaking Changes

By replacing the fetched results controller class with an asynchronous fetch request, we have broken a few pieces of the application. For example, tapping the checkmark of a to-do item doesn't seem to work any longer. While the database is being updated, the user interface doesn't reflect the change. The solution is fairly easy to fix and I'll leave it up to you to implement a solution. You should now have enough knowledge to understand the problem and find a suitable solution.

Conclusion

I'm sure you agree that asynchronous fetching is surprisingly easy to use. The heavy lifting is done by Core Data, which means that there's no need to manually merge the results of the asynchronous fetch request with the managed object context. Your only job is to update the user interface when the asynchronous fetch request hands you the results.

Tags:

Comments

Related Articles