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.
import UIKit import CoreData class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let ReuseIdentifierToDoCell = "ToDoCell" @IBOutlet weak var tableView: UITableView! var managedObjectContext: NSManagedObjectContext! var items: [NSManagedObject] = [] ... }
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.
func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 }
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count }
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) { // Fetch Record let record = items[indexPath.row] // Update Cell if let name = record.valueForKey("name") as? String { cell.nameLabel.text = name } if let done = record.valueForKey("done") as? Bool { cell.doneButton.selected = done } cell.didTapButtonHandler = { if let done = record.valueForKey("done") as? Bool { record.setValue(!done, forKey: "done") } } }
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if (editingStyle == .Delete) { // Fetch Record let record = items[indexPath.row] // Delete Record managedObjectContext.deleteObject(record) } }
We also need to change one line of code in the prepareForSegue(_:sender:)
method as shown below.
// Fetch Record let record = items[indexPath.row]
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.
override func viewDidLoad() { super.viewDidLoad() // Initialize Fetch Request let fetchRequest = NSFetchRequest(entityName: "Item") // Add Sort Descriptors fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)] // Initialize Asynchronous Fetch Request let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in self.processAsynchronousFetchResult(asynchronousFetchResult) }) } do { // Execute Asynchronous Fetch Request let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) print(asynchronousFetchResult) } catch { let fetchError = error as NSError print("\(fetchError), \(fetchError.userInfo)") } }
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.
// Initialize Fetch Request let fetchRequest = NSFetchRequest(entityName: "Item") // Add Sort Descriptors fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
To initialize an NSAsynchronousFetchRequest
instance, we invoke init(request:completionBlock:)
, passing in fetchRequest
and a completion block.
// Initialize Asynchronous Fetch Request let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in self.processAsynchronousFetchResult(asynchronousFetchResult) }) }
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.
do { // Execute Asynchronous Fetch Request let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) print(asynchronousFetchResult) } catch { let fetchError = error as NSError print("\(fetchError), \(fetchError.userInfo)") }
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.
func processAsynchronousFetchResult(asynchronousFetchResult: NSAsynchronousFetchResult) { if let result = asynchronousFetchResult.finalResult { // Update Items items = result as! [NSManagedObject] // Reload Table View tableView.reloadData() } }
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.
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' use_frameworks! pod 'SVProgressHUD', '~> 1.1'
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.
// Create Progress let progress = NSProgress(totalUnitCount: 1) // Become Current progress.becomeCurrentWithPendingUnitCount(1)
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.
// Create Progress let progress = NSProgress(totalUnitCount: 1) // Become Current progress.becomeCurrentWithPendingUnitCount(1) // Execute Asynchronous Fetch Request let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) as! NSAsynchronousFetchResult if let asynchronousFetchProgress = asynchronousFetchResult.progress { asynchronousFetchProgress.addObserver(self, forKeyPath: "completedUnitCount", options: NSKeyValueObservingOptions.New, context: nil) } // Resign Current progress.resignCurrent()
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.
// Initialize Asynchronous Fetch Request let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in // Dismiss Progress HUD SVProgressHUD.dismiss() // Process Asynchronous Fetch Result self.processAsynchronousFetchResult(asynchronousFetchResult) if let asynchronousFetchProgress = asynchronousFetchResult.progress { // Remove Observer asynchronousFetchProgress.removeObserver(self, forKeyPath: "completedUnitCount") } }) }
Before we implement observeValueForKeyPath(_:ofObject:change:context:)
, we need to show the progress HUD before creating the asynchronous fetch request.
// Show Progress HUD SVProgressHUD.showWithStatus("Fetching Data", maskType: .Gradient)
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.
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { if keyPath == "completedUnitCount" { dispatch_async(dispatch_get_main_queue(), { () -> Void in if let changes = change, number = changes["new"] { // Create Status let status = "Fetched \(number) Records" // Show Progress HUD SVProgressHUD.setStatus(status) } }) } }
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.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Populate Database populateDatabase() ... return true }
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.
private func populateDatabase() { // Helpers let userDefaults = NSUserDefaults.standardUserDefaults() guard userDefaults.objectForKey("didPopulateDatabase") == nil else { return } // Create Entity let entityDescription = NSEntityDescription.entityForName("Item", inManagedObjectContext: self.managedObjectContext) for index in 0...1000000 { // Initialize Record let record = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext) // Populate Record record.setValue(NSDate(), forKey: "createdAt") record.setValue("Item \(index)", forKey: "name") } // Save Changes saveManagedObjectContext() // Update User Defaults userDefaults.setBool(true, forKey: "didPopulateDatabase") }
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.
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.
Comments