Core Data is a framework I really enjoy working with. Even though Core Data isn't perfect, it's great to see that Apple continues to invest in the framework. This year, for example, Apple added the ability to batch delete records. In the previous article, we discussed batch updates. The idea underlying batch deletes is very similar as you'll learn in this tutorial.
1. The Problem
If a Core Data application needs to remove a large number of records, then it's faced with a problem. Even though there's no need to load a record into memory to delete it, that's simply how Core Data works. As we discussed in the previous article, this has a number of downsides. Before the introduction of batch updates, there was no proper solution for updating a large number of records. Before iOS 9 and OS X El Capitan, the same applied to batch deletes.
2. The Solution
While the NSBatchUpdateRequest
class was introduced in iOS 8 and OS X Yosemite, the NSBatchDeleteRequest
class was added only recently, alongside the release of iOS 9 and OS X El Capitan. Like its cousin, NSBatchUpdateRequest
, a NSBatchDeleteRequest
instance operates directly on one or more persistent stores.
Unfortunately, this means that batch deletes suffer from the same limitations batch updates do. Because a batch delete request directly affects a persistent store, the managed object context is ignorant of the consequences of a batch delete request. This also means that no validations are performed and no notifications are posted when the underlying data of a managed object changes as a result of a batch delete request. Despite these limitations, the delete rules for relationships are applied by Core Data.
3. How Does It Work?
In the previous tutorial, we added a feature to mark every to-do item as done. Let's revisit that application and add the ability to delete every to-do item that is marked as done.
Step 1: Project Setup
Download or clone the project from GitHub and open it in Xcode 7. Make sure the deployment target of the project is set to iOS 9 or higher to make sure the NSBatchDeleteRequest
class is available.
Step 2: Create Bar Button Item
Open ViewController.swift and declare a property deleteAllButton
of type UIBarButtonItem
. You can delete the checkAllButton
property since we won't be needing it in this tutorial.
import UIKit import CoreData class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate { let ReuseIdentifierToDoCell = "ToDoCell" @IBOutlet weak var tableView: UITableView! var managedObjectContext: NSManagedObjectContext! var deleteAllButton: UIBarButtonItem! ... }
Initialize the bar button item in the viewDidLoad()
method of the ViewController
class and set it as the left bar button item of the navigation item.
// Initialize Delete All Button deleteAllButton = UIBarButtonItem(title: "Delete All", style: .Plain, target: self, action: "deleteAll:") // Configure Navigation Item navigationItem.leftBarButtonItem = deleteAllButton
Step 3: Implement deleteAll(_:)
Method
Using the NSBatchDeleteRequest
class isn't difficult, but we need to take care of a few issues that are inherent to directly operating on a persistent store.
func deleteAll(sender: UIBarButtonItem) { // Create Fetch Request let fetchRequest = NSFetchRequest(entityName: "Item") // Configure Fetch Request fetchRequest.predicate = NSPredicate(format: "done == 1") // Initialize Batch Delete Request let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) // Configure Batch Update Request batchDeleteRequest.resultType = .ResultTypeCount do { // Execute Batch Request let batchDeleteResult = try managedObjectContext.executeRequest(batchDeleteRequest) as! NSBatchDeleteResult print("The batch delete request has deleted \(batchDeleteResult.result!) records.") // Reset Managed Object Context managedObjectContext.reset() // Perform Fetch try self.fetchedResultsController.performFetch() // Reload Table View tableView.reloadData() } catch { let updateError = error as NSError print("\(updateError), \(updateError.userInfo)") } }
Create Fetch Request
An NSBatchDeleteRequest
object is initialized with an NSFetchRequest
object. It's this fetch request that determines which records will be deleted from the persistent store(s). In deleteAll(_:)
, we create a fetch request for the Item entity. We set fetch request's predicate
property to make sure we only delete Item records that are marked as done.
// Create Fetch Request let fetchRequest = NSFetchRequest(entityName: "Item") // Configure Fetch Request fetchRequest.predicate = NSPredicate(format: "done == 1")
Because the fetch request determines which records will be deleted, we have all the power of the NSFetchRequest
class at our disposal, including setting a limit on the number of records, using sort descriptors, and specifying an offset for the fetch request.
Create Batch Request
As I mentioned earlier, the batch delete request is initialized with an NSFetchRequest
instance. Because the NSBatchDeleteRequest
class is an NSPersistentStoreRequest
subclass, we can set the request's resultType
property to specify what type of result we're interested in.
// Initialize Batch Delete Request let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) // Configure Batch Update Request batchDeleteRequest.resultType = .ResultTypeCount
The resultType
property of an NSBatchDeleteRequest
instance is of type NSBatchDeleteRequestResultType
.The NSBatchDeleteRequestResultType
enum defines three member variables:
-
ResultTypeStatusOnly
: This tells us whether the batch delete request was successful or unsuccessful. -
ResultTypeObjectIDs
: This gives us an array of theNSManagedObjectID
instances that correspond with the records that were deleted by the batch delete request.
-
ResultTypeCount
: By setting the request'sresultType
property toResultTypeCount
, we are given the number of records that were affected (deleted) by the batch delete request.
Execute Batch Update Request
You may recall from the previous tutorial that executeRequest(_:)
is a throwing method. This means that we need to wrap the method call in a do-catch
statement. The executeRequest(_:)
method returns a NSPersistentStoreResult
object. Because we're dealing with a batch delete request, we cast the result to an NSBatchDeleteResult
object. The result is printed to the console.
do { // Execute Batch Request let batchDeleteResult = try managedObjectContext.executeRequest(batchDeleteRequest) as! NSBatchDeleteResult print("The batch delete request has deleted \(batchDeleteResult.result!) records.") } catch { let updateError = error as NSError print("\(updateError), \(updateError.userInfo)") }
If you were to run the application, populate it with a few items, and tap the Delete All button, the user interface wouldn't be updated. I can assure you that the batch delete request did its work though. Remember that the managed object context is not notified in any way of the consequences of the batch delete request. Obviously, that's something we need to fix.
Updating the Managed Object Context
In the previous tutorial, we worked with the NSBatchUpdateRequest
class. We updated the managed object context by refreshing the objects in the managed object context that were affected by the batch update request.
We can't use the same technique for the batch delete request, because some objects are no longer represented by a record in the persistent store. We need to take drastic measures as you can see below. We call reset()
on the managed object context, which means that the managed object context starts with a clean slate.
do { // Execute Batch Request let batchDeleteResult = try managedObjectContext.executeRequest(batchDeleteRequest) as! NSBatchDeleteResult print("The batch delete request has deleted \(batchDeleteResult.result!) records.") // Reset Managed Object Context managedObjectContext.reset() // Perform Fetch try self.fetchedResultsController.performFetch() // Reload Table View tableView.reloadData() } catch { let updateError = error as NSError print("\(updateError), \(updateError.userInfo)") }
This also means that the fetched results controller needs to perform a fetch to update the records it manages for us. To update the user interface, we invoke reloadData()
on the table view.
4. Saving State Before Deleting
It's important to be careful whenever you directly interact with a persistent store(s). Earlier in this series, I wrote that it isn't necessary to save the changes of a managed object context whenever you add, update, or delete a record. That statement still holds true, but it also has consequences when working with NSPersistentStoreRequest
subclasses.
Before we continue, I'd like to seed the persistent store with dummy data so we have something to work with. This makes it easier to visualize what I'm about to explain. Add the following helper method to ViewController.swift and invoke it in viewDidLoad()
.
// MARK: - // MARK: Helper Methods private func seedPersistentStore() { // Create Entity Description let entityDescription = NSEntityDescription.entityForName("Item", inManagedObjectContext: managedObjectContext) for i in 0...15 { // Initialize Record let record = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext) // Populate Record record.setValue((i % 3) == 0, forKey: "done") record.setValue(NSDate(), forKey: "createdAt") record.setValue("Item \(i + 1)", forKey: "name") } do { // Save Record try managedObjectContext?.save() } catch { let saveError = error as NSError print("\(saveError), \(saveError.userInfo)") } }
In seedPersistentStore()
, we create a few records and mark every third item as done. Note that we call save()
on the managed object context at the end of this method to make sure the changes are pushed to the persistent store. In viewDidLoad()
, we seed the persistent store.
override func viewDidLoad() { super.viewDidLoad() ... // Seed Persistent Store seedPersistentStore() }
Run the application and tap the Delete All button. The records that are marked as done should be deleted. What happens if you mark a few of the remaining items as done and tap the Delete All button again. Are these items also removed? Can you guess why that is?
The batch delete request directly interacts with the persistent store. When an item is marked as done, however, the change isn't immediately pushed to the persistent store. We don't call save()
on the managed object context every time the user marks an items as done. We only do this when the application is pushed to the background and when it is terminated (see AppDelegate.swift).
The solution is simple. To fix the issue, we need to save the changes of the managed object context before executing the batch delete request. Add the following lines to the deleteAll(_:)
method and run the application again to test the solution.
func deleteAll(sender: UIBarButtonItem) { if managedObjectContext.hasChanges { do { try managedObjectContext.save() } catch { let saveError = error as NSError print("\(saveError), \(saveError.userInfo)") } } ... }
Conclusion
The NSPersistentStoreRequest
subclasses are a very useful addition to the Core Data framework, but I hope it's clear that they should only be used when absolutely necessary. Apple only added the ability to directly operate on persistent stores to patch the weaknesses of the framework, but the advice is to use them sparingly.
Comments