An Interest In:
Web News this Week
- April 29, 2024
- April 28, 2024
- April 27, 2024
- April 26, 2024
- April 25, 2024
- April 24, 2024
- April 23, 2024
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.
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.
Original Link:
TutsPlus - Code
Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.More About this Source Visit TutsPlus - Code