Thursday, November 26, 2015

Core Data and Swift: Batch Deletes

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.

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.

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.

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.

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.

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 the NSManagedObjectID instances  that correspond with the records that were deleted by the batch delete request.
  • ResultTypeCount: By setting the request's resultType property to ResultTypeCount, 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.

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.

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().

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.

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.

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.


by Bart Jacobs via Envato Tuts+ Code

No comments:

Post a Comment