4.
Intermediate Fetching
Written by Pietro Rea
In the first three chapters of this book, you began to explore the foundations of Core Data, including very basic methods of saving and fetching data within the Core Data persistent store.
To this point, you’ve mostly performed simple, unrefined fetches such as “fetch all BowTie entities.” Sometimes this is all you need to do. Often, you’ll want to exert more control over how you retrieve information from Core Data.
Building on what you’ve learned so far, this chapter dives deep into the topic of fetching. Fetching is a large topic in Core Data, and you have many tools at your disposal. By the end of this chapter, you’ll know how to:
- Fetch only what you need to
- Refine your fetched results using predicates
- Fetch in the background to avoid blocking the UI
- Avoid unnecessary fetching by updating objects directly in the persistent store
This chapter is a toolbox sampler; its aim is to expose you to many fetching techniques, so when the time comes, you’ll know what tool to use.
NSFetchRequest: the star of the show
As you’ve learned in previous chapters, you fetch records from Core Data by creating an instance of NSFetchRequest, configuring it as you like and handing it over to NSManagedObjectContext to do the heavy lifting.
Seems simple enough, but there are actually five different ways to get hold of a fetch request. Some are more popular than others, but you’ll likely encounter all of them at some point as a Core Data developer.
Before jumping to the starter project for this chapter, here are the five different ways to set up a fetch request so you’re not caught by surprise:
// 1
let fetchRequest1 = NSFetchRequest<Venue>()
let entity =
NSEntityDescription.entity(forEntityName: "Venue",
in: managedContext)!
fetchRequest1.entity = entity
// 2
let fetchRequest2 = NSFetchRequest<Venue>(entityName: "Venue")
// 3
let fetchRequest3: NSFetchRequest<Venue> = Venue.fetchRequest()
// 4
let fetchRequest4 =
managedObjectModel.fetchRequestTemplate(forName: "venueFR")
// 5
let fetchRequest5 =
managedObjectModel.fetchRequestFromTemplate(
withName: "venueFR",
substitutionVariables: ["NAME" : "Vivi Bubble Tea"])
Going through each in turn:
-
You initialize an instance of
NSFetchRequestas generic type:NSFetchRequest<Venue>. At a minimum, you must specify aNSEntityDescriptionfor the fetch request. In this case, the entity isVenue. You initialize an instance ofNSEntityDescriptionand use it to set the fetch request’sentityproperty. -
Here you use
NSFetchRequest’s convenience initializer. It initializes a new fetch request and sets itsentityproperty in one step. You simply need to provide a string for the entity name rather than a full-fledgedNSEntityDescription. -
Just as the second example was a contraction of the first, the third is a contraction of the second. When you generate an
NSManagedObjectsubclass, this step also generates a class method that returns anNSFetchRequestalready set up to fetch corresponding entity types. This is whereVenue.fetchRequest()comes from. This code lives in Venue+CoreDataProperties.swift. -
In the fourth example, you retrieve your fetch request from your
NSManagedObjectModel. You can configure and store commonly used fetch requests in Xcode’s data model editor. You’ll learn how to do this later in the chapter. -
The last case is similar to the fourth. Retrieve a fetch request from your managed object model, but this time, you pass in some extra variables. These “substitution” variables are used in a predicate to refine your fetched results.
The first three examples are the simple cases you’ve already seen. You’ll see even more of these simple cases in the rest of this chapter, in addition to stored fetch requests and other tricks of NSFetchRequest!
Note: If you’re not already familiar with it,
NSFetchRequestis a generic type. If you inspectNSFetchRequest‘s initializer, you’ll notice it takes in type as a parameter<ResultType : NSFetchRequestResult>.
ResultTypespecifies the type of objects you expect as a result of the fetch request. For example, if you’re expecting an array ofVenueobjects, the result of the fetch request is now going to be[Venue]instead of[Any]. This is helpful because you don’t have to cast down to[Venue]anymore.
Introducing the BubbleTea app
This chapter’s sample project is a bubble tea app. For those of you who don’t know about bubble tea (also known as “boba tea”), it’s a Taiwanese tea-based drink containing large tapioca pearls. It’s very yummy!
You can think of this bubble tea app as an ultra-niche Yelp. Using the app, you can find locations near you selling your favorite Taiwanese drink.
For this chapter, you’ll only be working with static venue data from Foursquare: that’s about 30 locations in New York City that sell bubble tea. You’ll use this data to build the filter/sort screen to arrange the list of static venues as you see fit.
Go to this chapter’s files and open BubbleTeaFinder.xcodeproj. Build and run the starter project.
You’ll see the following:
The sample app consists of a number of table view cells with static information. Although the sample project isn’t very exciting at the moment, there’s a lot of setup already done for you.
Open the project navigator and take a look at the full list of files in the starter project:
It turns out most of the Core Data setup you had to do in the first section of the book comes ready for you to use. Below is a quick overview of the components you get in the starter project, grouped into categories:
-
Seed data: seed.json is a JSON file containing real-world venue data for venues in New York City serving bubble tea. Since this is real data coming from Foursquare, the structure is more complex than previous seed data used in this book.
-
Data model: Click on BubbleTeaFinder.xcdatamodeld to open Xcode’s model editor. The most important entity is
Venue. It contains attributes for a venue’s name, phone number and the number of specials it’s offering at the moment.Since the JSON data is rather complex, the data model breaks down a venue’s information into other entities. These are
Category,Location,PriceInfoandStats. For example,Locationhas attributes for city, state, country, and others. -
Managed object subclasses: All the entities in your data model also have corresponding
NSManagedObjectsubclasses. These are Venue+CoreDataClass.swift, Location+CoreDataClass.swift, PriceInfo+CoreDataClass.swift, Category+CoreDataClass.swift and Stats+CoreDataClass.swift. You can find these in the NSManagedObject group along with their accompanying EntityName+CoreDataProperties.swift file. -
CoreDataStack: As in previous chapters, this object wraps an
NSPersistentContainerobject, which itself contains the cadre of Core Data objects known as the “stack”; the context, the model, the persistent store and the persistent store coordinator. No need to set this up — it comes ready-to-use. -
View Controllers: The initial view controller that shows you the list of venues is ViewController.swift. On first launch, the initial view controller reads from seed.json, creates corresponding Core Data objects and saves them to the persistent store. Tapping the Filter button on the top-right brings up FilterViewController.swift. There’s not much going on here at the moment. You’ll add code to these two files throughout this chapter.
When you first launched the sample app, you saw only static information. However, your app delegate had already read the seed data from seed.json, parsed it into Core Data objects and saved them into the persistent store.
Your first task will be to fetch this data and display it in the table view. This time, you’ll do it with a twist.
Stored fetch requests
As previously mentioned, you can store frequently used fetch requests right in the data model. Not only does this make them easier to access, but you also get the benefit of using a GUI-based tool to set up the fetch request parameters.
Open BubbleTeaFinder.xcdatamodeld and long-click the Add Entity button:
Select Add Fetch Request from the menu. This will create a new fetch request on the left-side bar and take you to a special fetch request editor:
Note: You can click on the newly created fetch request on the left-hand sidebar to change its name.
You can make your fetch request as general or as specific as you want using the visual tool in Xcode’s data model editor. To start, create a fetch request that retrieves all Venue objects from the persistent store.
You only need to make one change here: click the dropdown menu next to Fetch all and select Venue.
That’s all you need to do. If you wanted to refine your fetch request with an additional predicate, you could also add conditions from the fetch request editor.
Time to take your newly created fetch request out for a spin. Open ViewController.swift and add the following two properties below coreDataStack:
var fetchRequest: NSFetchRequest<Venue>?
var venues: [Venue] = []
The first property will hold your fetch request. The second property is the array of Venue objects you’ll use to populate the table view.
Next, add the following to the end of viewDidLoad():
guard let model =
coreDataStack.managedContext
.persistentStoreCoordinator?.managedObjectModel,
let fetchRequest = model
.fetchRequestTemplate(forName: "FetchRequest")
as? NSFetchRequest<Venue> else {
return
}
self.fetchRequest = fetchRequest
fetchAndReload()
Doing this connects the fetchRequest property you just set up to the one you created using Xcode’s data model editor. There are three things to remember here:
-
Unlike other ways of getting a fetch request, this one involves the managed object model. This is why you must go through the
coreDataStackproperty to retrieve your fetch request. -
As you saw in the previous chapter, you constructed
CoreDataStackso only the managed context is public. To retrieve the managed object model, you have to go through the managed context’s persistent store coordinator. -
NSManagedObjectModel’sfetchRequestTemplate(forName:)takes a string identifier. This identifier must exactly match the name you chose for your fetch request in the model editor. Otherwise, your app will throw an exception and crash.
The last line calls a method you haven’t defined yet, so Xcode will complain about it. To fix that, add the following extension above the UITableViewDataSource extension:
// MARK: - Helper methods
extension ViewController {
func fetchAndReload() {
guard let fetchRequest = fetchRequest else {
return
}
do {
venues =
try coreDataStack.managedContext.fetch(fetchRequest)
tableView.reloadData()
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
}
As its name suggests, fetchAndReload() executes the fetch request and reloads the table view. Other methods in this class will need to see the fetched objects, so you store the fetched results in the venues property you defined earlier.
There’s one more thing you have to do before you can run the sample project: hook up the table view’s data source with the fetched Venue objects.
In the UITableViewDataSource extension, replace the placeholder implementations of tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:) with the following:
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
venues.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell =
tableView.dequeueReusableCell(
withIdentifier: venueCellIdentifier, for: indexPath)
let venue = venues[indexPath.row]
cell.textLabel?.text = venue.name
cell.detailTextLabel?.text = venue.priceInfo?.priceCategory
return cell
}
You’ve implemented these methods many times in this book, so you’re probably familiar with what they do. The first method, tableView(_:numberOfRowsInSection:), matches the number of cells in the table view with the number of fetched objects in the venues array.
The second method, tableView(_:cellForRowAt:), dequeues a cell for a given index path and populates it with the information of the corresponding Venue in the venues array. In this case, the main label gets the venue’s name and the detail label gets a price category that is one of three possible values: $, $$ or $$$.
Build and run the project, and you’ll see the following:
Scroll down the list of bubble tea venues. These are all real places in New York City that sell the delicious drink.
Note: When should you store fetch requests in your data model?
If you know you’ll be making the same fetch over and over in different parts of your app, you can use this feature to save you from writing the same code multiple times. A drawback of stored fetch requests is that there is no way to specify a sort order for the results. Therefore, the list of venues you saw may have been in a different order than in the book.
Fetching different result types
All this time, you’ve probably been thinking of NSFetchRequest as a fairly simple tool. You give it some instructions and you get some objects in return. What else is there to it?
If this is the case, you’ve been underestimating this class. NSFetchRequest is the multi-function Swiss army knife of the Core Data framework!
You can use it to fetch individual values, compute statistics on your data such as the average, minimum, maximum, and more.
How is this possible, you ask? NSFetchRequest has a property named resultType. So far, you’ve only used the default value, .managedObjectResultType. Here are all the possible values for a fetch request’s resultType:
-
.managedObjectResultType:Returns managed objects (default value). -
.countResultType: Returns the count of the objects matching the fetch request. -
.dictionaryResultType: This is a catch-all return type for returning the results of different calculations. -
.managedObjectIDResultType: Returns unique identifiers instead of full-fledged managed objects.
Let’s go back to the sample project and apply these concepts in practice.
With the sample project running, tap Filter in the top-right corner to bring up the UI for the filter screen.
You won’t implement the actual filters or sorts right now. Instead, you’ll focus on the following four labels:
The filter screen is divided into three sections: Price, Most Popular and Sort By. That last section is not technically made up of “filters”, but sorting usually goes hand-in-hand with filters, so you’ll leave it like that.
Below each price filter is space for the total number of venues that fall into that price category. Similarly, there’s a spot for the total number of deals across all venues. You’ll implement these next.
Returning a count
Open FilterViewController.swift and add the following below import UIKit:
import CoreData
Next, add the following property below the last @IBOutlet property:
// MARK: - Properties
var coreDataStack: CoreDataStack!
This will hold a reference to the CoreDataStack object you’ve been using in ViewController.swift.
Next, open ViewController.swift and replace the prepare(for:sender:) implementation with the following:
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
guard segue.identifier == filterViewControllerSegueIdentifier,
let navController = segue.destination
as? UINavigationController,
let filterVC = navController.topViewController
as? FilterViewController else {
return
}
filterVC.coreDataStack = coreDataStack
}
The new line of code propagates the CoreDataStack object from ViewController to FilterViewController. The filter screen is now ready to use Core Data.
Open FilterViewController.swift and add the following lazy property below coreDataStack:
lazy var cheapVenuePredicate: NSPredicate = {
return NSPredicate(format: "%K == %@",
#keyPath(Venue.priceInfo.priceCategory), "$")
}()
You’ll use this lazily-instantiated NSPredicate to calculate the number of venues in the lowest price category.
Note:
NSPredicatesupports string-based key paths. This is why you can drill down from theVenueentity into thePriceInfoentity usingpriceInfo.priceCategory, and use the#keyPathkeyword to get safe, compile-time checked values for the key path.As of this writing,
NSPredicatedoes not support Swift 4 style key paths such as\Venue.priceInfo.priceCategory.
Next, add the following extension below the UITableViewDelegate extension:
// MARK: - Helper methods
extension FilterViewController {
func populateCheapVenueCountLabel() {
let fetchRequest =
NSFetchRequest<NSNumber>(entityName: "Venue")
fetchRequest.resultType = .countResultType
fetchRequest.predicate = cheapVenuePredicate
do {
let countResult =
try coreDataStack.managedContext.fetch(fetchRequest)
let count = countResult.first?.intValue ?? 0
let pluralized = count == 1 ? "place" : "places"
firstPriceCategoryLabel.text =
"\(count) bubble tea \(pluralized)"
} catch let error as NSError {
print("count not fetched \(error), \(error.userInfo)")
}
}
}
This extension provides populateCheapVenueCountLabel() which creates a fetch request to fetch Venue entities. You then set the result type to .countResultType and set the fetch request’s predicate to cheapVenuePredicate. Notice that for this to work correctly, the fetch request’s type parameter has to be NSNumber, not Venue.
When you set a fetch result’s result type to .countResultType, the return value becomes a Swift array containing a single NSNumber. The integer inside the NSNumber is the total count you’re looking for.
Once again, you execute the fetch request against CoreDataStack’s NSManagedObjectContext property. Then you extract the integer from the resulting NSNumber and use it to populate firstPriceCategoryLabel.
Before you run the sample app, add the following to the bottom of viewDidLoad():
populateCheapVenueCountLabel()
Now build and run to test if these changes took effect. Tap Filter to bring up the filter/sort menu:
The label under the first price filter now says “27 bubble tea places.” Hooray! You’ve successfully used NSFetchRequest to calculate a count.
Note: You may be thinking that you could have just as easily fetched the actual Venue objects and gotten the count from the array’s
countproperty. That’s true. Fetching counts instead of objects is mainly a performance optimization. For example, if you had census data for New York City and wanted to know how many people lived in its metropolitan area, would you prefer Core Data gave you the number 8,300,000 (an integer) or an array of 8,300,000 records?Obviously, getting the count directly is more memory-efficient. There’s a whole chapter devoted to Core Data performance. If you want to learn more about performance optimization in Core Data, check out Chapter 8, “Measuring & Boosting Performance.”
Now that you’re acquainted with the count result type, you can quickly implement the count for the second price category filter. Add the following lazy property below cheapVenuePredicate:
lazy var moderateVenuePredicate: NSPredicate = {
return NSPredicate(format: "%K == %@",
#keyPath(Venue.priceInfo.priceCategory), "$$")
}()
This NSPredicate is almost identical to the cheap venue predicate, except this one matches against $$ instead of $. Similarly, add the following method below populateCheapVenueCountLabel():
func populateModerateVenueCountLabel() {
let fetchRequest =
NSFetchRequest<NSNumber>(entityName: "Venue")
fetchRequest.resultType = .countResultType
fetchRequest.predicate = moderateVenuePredicate
do {
let countResult =
try coreDataStack.managedContext.fetch(fetchRequest)
let count = countResult.first?.intValue ?? 0
let pluralized = count == 1 ? "place" : "places"
secondPriceCategoryLabel.text =
"\(count) bubble tea \(pluralized)"
} catch let error as NSError {
print("count not fetched \(error), \(error.userInfo)")
}
}
Finally, add the following line to the bottom of viewDidLoad() to invoke your newly defined method:
populateModerateVenueCountLabel()
Build and run the sample project. As before, tap Filter on the top right to reach the filter/sort screen:
Great news for bubble tea lovers! Only two places are moderately expensive. Bubble tea as a whole seems to be quite accessible.
An alternate way to fetch a count
Now that you’re familiar with .countResultType, it’s a good time to mention that there’s an alternate API for fetching a count directly from Core Data.
Since there’s one more price category count to implement, you’ll use this alternate API now.
Add the follow lazy property below moderateVenuePredicate:
lazy var expensiveVenuePredicate: NSPredicate = {
return NSPredicate(format: "%K == %@",
#keyPath(Venue.priceInfo.priceCategory), "$$$")
}()
Next, implement the following method below populateModerateVenueCountLabel():
func populateExpensiveVenueCountLabel() {
let fetchRequest: NSFetchRequest<Venue> = Venue.fetchRequest()
fetchRequest.predicate = expensiveVenuePredicate
do {
let count =
try coreDataStack.managedContext.count(for: fetchRequest)
let pluralized = count == 1 ? "place" : "places"
thirdPriceCategoryLabel.text =
"\(count) bubble tea \(pluralized)"
} catch let error as NSError {
print("count not fetched \(error), \(error.userInfo)")
}
}
Like the previous two scenarios, you create a fetch request for retrieving Venue objects.
Next, you set the predicate that you defined as a lazy property earlier: expensiveVenuePredicate.
The difference between this scenario and the last two is that here, you don’t set the result type to .countResultType. Rather than the usual fetch(_:), you use NSManagedObjectContext’s method count(for:) instead.
The return value for count(for:) is an integer that you can use directly to populate the third price category label. Finally, add the following line to the bottom of viewDidLoad() to invoke your newly defined method:
populateExpensiveVenueCountLabel()
Build and run to see if your latest changes took effect.
The filter/sort screen should look like this:
There’s only one bubble tea venue that falls into the $$$ category. Maybe they use real pearls instead of tapioca?
Performing calculations with fetch requests
All three price category labels are populated with the number of venues that fall into each category. The next step is to populate the label under “Offering a deal.” It currently says “0 total deals.” That can’t be right!
Where exactly does this information come from? Venue has a specialCount attribute that captures the number of deals the venue is currently offering. Unlike the labels under the price category, you now need to know the total sum of deals across all venues since a particularly savvy venue could have many deals at once.
The naïve approach would be to load all venues into memory and sum their deals using a for loop. If you’re hoping for a better way, you’re in luck: Core Data has built-in support for a number of different functions such as average, sum, min and max.
Open FilterViewController.swift, and add the following method below populateExpensiveVenueCountLabel():
func populateDealsCountLabel() {
// 1
let fetchRequest =
NSFetchRequest<NSDictionary>(entityName: "Venue")
fetchRequest.resultType = .dictionaryResultType
// 2
let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"
// 3
let specialCountExp =
NSExpression(forKeyPath: #keyPath(Venue.specialCount))
sumExpressionDesc.expression =
NSExpression(forFunction: "sum:",
arguments: [specialCountExp])
sumExpressionDesc.expressionResultType =
.integer32AttributeType
// 4
fetchRequest.propertiesToFetch = [sumExpressionDesc]
// 5
do {
let results =
try coreDataStack.managedContext.fetch(fetchRequest)
let resultDict = results.first
let numDeals = resultDict?["sumDeals"] as? Int ?? 0
let pluralized = numDeals == 1 ? "deal" : "deals"
numDealsLabel.text = "\(numDeals) \(pluralized)"
} catch let error as NSError {
print("count not fetched \(error), \(error.userInfo)")
}
}
This method contains a few classes you’ve not encountered in the book before, so here is each explained in turn:
-
You begin by creating your typical fetch request for retrieving
Venueobjects. Next, you specify the result type to be.dictionaryResultType. -
You create an
NSExpressionDescriptionto request the sum, and give it the namesumDealsso you can read its result out of the result dictionary you’ll get back from the fetch request. -
You give the expression description an
NSExpressionto specify you want the sum function. Next, give that expression anotherNSExpressionto specify what property you want to sum over — in this case,specialCount. Finally, you have to set the return data type of your expression description, so you set it tointeger32AttributeType. -
You tell your original fetch request to fetch the
sumby setting itspropertiesToFetchproperty to the expression description you just created. -
Finally, execute the fetch request in the usual
do-catchstatement. The result type is anNSDictionaryarray, so you retrieve the result of your expression using your expression description’s name (sumDeals) and you’re done!
Note: What other functions does Core Data support? To name a few: count, min, max, average, median, mode, absolute value and many more. For a comprehensive list, check out Apple’s documentation for
NSExpression.
Fetching a calculated value from Core Data requires you to follow many, often unintuitive steps, so make sure you have a good reason for using this technique, such as performance considerations. Finally, add the following line to the bottom of viewDidLoad():
populateDealsCountLabel()
Build the sample project and open the filter/sort screen to verify your changes.
Great! There are 12 deals across all venues stored in Core Data.
You’ve now used three of the four supported NSFetchRequest result types: .managedObjectResultType, .countResultType and .dictionaryResultType.
The remaining result type is .managedObjectIDResultType. When you fetch with this type, the result is an array of NSManagedObjectID objects rather the actual managed objects they represent. An NSManagedObjectID is a compact universal identifier for a managed object. It works like the primary key in the database!
Prior to iOS 5, fetching by ID was popular because NSManagedObjectID was thread-safe and using it helped developers implement the thread confinement concurrency model.
Now that thread confinement has been deprecated in favor of more modern concurrency models, there’s little reason to fetch by object ID anymore.
Note: You can set up multiple managed object contexts to run concurrent operations and keep long-running operations off the main thread. For more information, check out Chapter 9, “Multiple Managed Object Contexts.”
You’ve gotten a taste of all the things a fetch request can do for you. But just as important as the information a fetch request returns, is the information it doesn’t return. For practical reasons, you have to cap the incoming data at some point.
Why? Imagine a perfectly connected object graph, one where each Core Data object is connected to every other object through a series of relationships. If Core Data didn’t put limits on the information a fetch request returned, you’d be fetching the entire object graph every single time! That’s not memory efficient.
You can manually limit the information you get back from a fetch request. For example, NSFetchRequest supports fetching batches. You can use the properties fetchBatchSize, fetchLimit and fetchOffset to control the batching behavior.
Core Data also tries to minimize its memory consumption for you by using a technique called faulting. A fault is a placeholder object representing a managed object that hasn’t yet been fully brought into memory.
Another way to limit your object graph is to use predicates, as you’ve done to populate the venue count labels above. Let’s add the filters to the sample app using predicates.
Open FilterViewController.swift, and add the following protocol declaration above your class definition:
protocol FilterViewControllerDelegate: class {
func filterViewController(
filter: FilterViewController,
didSelectPredicate predicate: NSPredicate?,
sortDescriptor: NSSortDescriptor?)
}
This protocol defines a delegate method that will notify the delegate when the user selects a new sort/filter combination.
Next, add the following three properties below coreDataStack:
weak var delegate: FilterViewControllerDelegate?
var selectedSortDescriptor: NSSortDescriptor?
var selectedPredicate: NSPredicate?
This first property will hold a reference to FilterViewController’s delegate. It’s a weak property instead of a strongly-retained property in order to avoid retain cycles. The second and third properties will hold references to the currently selected NSSortDescriptor and NSPredicate, respectively.
Next, implement search(_:) as shown below:
@IBAction func search(_ sender: UIBarButtonItem) {
delegate?.filterViewController(
filter: self,
didSelectPredicate: selectedPredicate,
sortDescriptor: selectedSortDescriptor)
dismiss(animated: true)
}
This means every time you tap Search in the top-right corner of the filter/sort screen, you’ll notify the delegate of your selection and dismiss the filter/sort screen to reveal the list of venues behind it.
You need to make one more change in this file. Find tableView(_:didSelectRowAt:) and implement it as shown below:
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
// Price section
switch cell {
case cheapVenueCell:
selectedPredicate = cheapVenuePredicate
case moderateVenueCell:
selectedPredicate = moderateVenuePredicate
case expensiveVenueCell:
selectedPredicate = expensiveVenuePredicate
default: break
}
cell.accessoryType = .checkmark
}
When the user taps on any of the first three price category cells, this method will map the selected cell to the appropriate predicate. You store a reference to this predicate in selectedPredicate so it’s ready when you notify the delegate of the user’s selection.
Next, open ViewController.swift and add the following extension to conform to the FilterViewControllerDelegate protocol:
// MARK: - FilterViewControllerDelegate
extension ViewController: FilterViewControllerDelegate {
func filterViewController(
filter: FilterViewController,
didSelectPredicate predicate: NSPredicate?,
sortDescriptor: NSSortDescriptor?) {
guard let fetchRequest = fetchRequest else {
return
}
fetchRequest.predicate = nil
fetchRequest.sortDescriptors = nil
fetchRequest.predicate = predicate
if let sort = sortDescriptor {
fetchRequest.sortDescriptors = [sort]
}
fetchAndReload()
}
}
Adding the FilterViewControllerDelegate Swift extension tells the compiler that this class will conform to this protocol. This delegate method fires every time the user selects a new filter/sort combination.
Here, you reset your fetch request’s predicate and sortDescriptors, then set the predicate and sort descriptor passed into the method and reload the data.
There’s one more thing you need to do before you can test your price category filters. Find prepare(for:sender:) and add the following line to the end of the method:
filterVC.delegate = self
This formally sets ViewController as FilterViewController’s delegate.
Build and run the sample project. Go to the Filter screen, tap the first price category cell ($) and then tap Search in the top-right corner.
Your app crashes with the following error message in the console:
2020-09-20 11:47:40.872640-0400 BubbleTeaFinder[65767:8506463] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Can't modify a named fetch request in an immutable model.'
What happened? Earlier in the chapter, you defined your fetch request in the data model. It turns out if you use that technique, the fetch request becomes immutable. You can’t change its predicate at runtime, or else you’ll crash spectacularly. If you want to modify the fetch request in any way, you have to do it in the data model editor in advance.
Open ViewController.swift, and replace viewDidLoad() with the following:
override func viewDidLoad() {
super.viewDidLoad()
importJSONSeedDataIfNeeded()
fetchRequest = Venue.fetchRequest()
fetchAndReload()
}
You removed the lines that retrieves the fetch request from the template in the managed object model. Instead, you get an instance of NSFetchRequest directly from the Venue entity.
Build and run the sample app one more time. Go to the Filter screen, tap the second price category cell ($$) and then tap Search in the top-right corner.
This is the result:
As expected, there are only two venues in this category. Test the first ($) and third ($$$) price category filters as well, making sure the filtered list contains the correct number of venues for each.
You’ll practice writing a few more predicates for the remaining filters. The process is similar to what you’ve done already, so this time you’ll do it with less explanation.
Open FilterViewController.swift and add these three lazy properties below expensiveVenuePredicate:
lazy var offeringDealPredicate: NSPredicate = {
return NSPredicate(format: "%K > 0",
#keyPath(Venue.specialCount))
}()
lazy var walkingDistancePredicate: NSPredicate = {
return NSPredicate(format: "%K < 500",
#keyPath(Venue.location.distance))
}()
lazy var hasUserTipsPredicate: NSPredicate = {
return NSPredicate(format: "%K > 0",
#keyPath(Venue.stats.tipCount))
}()
The first predicate specifies venues currently offering one or more deals, the second predicate specifies venues less than 500 meters away from your current location and the third predicate specifies venues that have at least one user tip.
Note: So far in the book, you’ve written predicates with a single condition. You should also know that you can write predicates that check two conditions instead of one by using compound predicate operators such as AND, OR and NOT.
Alternatively, you can string two simple predicates into one compound predicate by using the class
NSCompoundPredicate.
NSPredicateisn’t technically part of Core Data (it’s part ofFoundation) so this book won’t cover it in depth, but you can seriously improve your Core Data chops by learning the ins and outs of this nifty class. For more information, make sure to check out Apple’s Predicate Programming Guide:
Next, scroll down to tableView(_:didSelectRowAt:). You’re going to add three more cases to the switch statement you added earlier:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
switch cell {
// Price section
case cheapVenueCell:
selectedPredicate = cheapVenuePredicate
case moderateVenueCell:
selectedPredicate = moderateVenuePredicate
case expensiveVenueCell:
selectedPredicate = expensiveVenuePredicate
// Most Popular section
case offeringDealCell:
selectedPredicate = offeringDealPredicate
case walkingDistanceCell:
selectedPredicate = walkingDistancePredicate
case userTipsCell:
selectedPredicate = hasUserTipsPredicate
default: break
}
cell.accessoryType = .checkmark
}
Above, you added cases for offeringDealCell, walkingDistanceCell and userTipsCell. These are the three new filters for which you’re now adding support.
That’s all you need to do. Build and run the sample app. Go to the Filters page, select the Offering a deal filter and tap Search:
You’ll see a total of six venues. Note that since you didn’t specify a sort descriptor, your list of venues may be in a different order than the venues in the screenshot. You can verify these venues have specials by looking them up in seed.json. For example, City Wing Cafe is currently offering four specials. Woo-hoo!
Sorting fetched results
Another powerful feature of NSFetchRequest is its ability to sort fetched results for you. It does this by using yet another handy Foundation class, NSSortDescriptor. These sorts happen at the SQLite level, not in memory. This makes sorting in Core Data fast and efficient.
In this section, you’ll implement four different sorts to complete the filter/sort screen.
Open FilterViewController.swift and add the following three lazy properties below hasUserTipsPredicate:
lazy var nameSortDescriptor: NSSortDescriptor = {
let compareSelector =
#selector(NSString.localizedStandardCompare(_:))
return NSSortDescriptor(key: #keyPath(Venue.name),
ascending: true,
selector: compareSelector)
}()
lazy var distanceSortDescriptor: NSSortDescriptor = {
return NSSortDescriptor(
key: #keyPath(Venue.location.distance),
ascending: true)
}()
lazy var priceSortDescriptor: NSSortDescriptor = {
return NSSortDescriptor(
key: #keyPath(Venue.priceInfo.priceCategory),
ascending: true)
}()
The way to add sort descriptors is very similar to the way you added filters. Each sort descriptor maps to one of these three lazy NSSortDescriptor properties.
To initialize an instance of NSSortDescriptor you need three things: a key path to specify the attribute which you want to sort, a specification of whether the sort is ascending or descending and an optional selector to perform the comparison operation.
Note: If you’ve worked with
NSSortDescriptorbefore, then you probably know there’s a block-based API that takes a comparator instead of a selector. Unfortunately, Core Data doesn’t support this method of defining a sort descriptor.The same thing goes for the block-based method of defining a
NSPredicate. Core Data doesn’t support this either. The reason is filtering and sorting happens in the SQLite database, so the predicate/sort descriptor has to match nicely to something that can be written as an SQL statement.
The three sort descriptors are going to sort by name, distance and price category, respectively, in ascending order. Before moving on, take a closer look at the first sort descriptor, nameSortDescriptor. The initializer takes in an optional selector, NSString.localizedStandardCompare(_:). What is that?
Any time you’re sorting user-facing strings, Apple recommends that you pass in NSString.localizedStandardCompare(_:) to sort according to the language rules of the current locale. This means sort will “just work” and do the right thing for languages with special characters. It’s the little things that matter, bien sûr!
Next, find tableView(_:didSelectRowAt:) and add the following cases to the end of the switch statement above the default case:
// Sort By section
case nameAZSortCell:
selectedSortDescriptor = nameSortDescriptor
case nameZASortCell:
selectedSortDescriptor =
nameSortDescriptor.reversedSortDescriptor
as? NSSortDescriptor
case distanceSortCell:
selectedSortDescriptor = distanceSortDescriptor
case priceSortCell:
selectedSortDescriptor = priceSortDescriptor
Like before, this switch statement matches the user tapped cell with the appropriate sort descriptor, so it’s ready to pass to the delegate when the user taps Search.
The only wrinkle is the nameZA sort descriptor. Rather than creating a separate sort descriptor, you can reuse the one for A-Z and simply call the method reversedSortDescriptor. How handy!
Everything else is hooked up for you to test the sorts you just implemented. Build and run the sample app and go to the Filter screen. Tap the Name (Z-A) sort and then tap Search. You’ll see search results ordered like so:
No, you’re not seeing double. There really are seven Vivi Bubble Tea venues in the data set — it’s a popular bubble tea chain in New York City.
As you scroll down the table view, you’ll see the app has indeed sorted the venues alphabetically from Z to A.
You’ve now completed your Filter screen, setting it up so the user can combine any one filter with any one sort. Try different combinations to see what you get. The venue cell doesn’t show much information, so if you need to verify a sort, you can go straight to the source and consult seed.json.
Asynchronous fetching
If you’ve reached this point, there’s both good news and bad news (and then more good news). The good news is you’ve learned a lot about what you can do with a plain NSFetchRequest. The bad news is that every fetch request you’ve executed so far has blocked the main thread while you waited for the results to come back.
When you block the main thread, it makes the screen unresponsive to incoming touches and creates a slew of other problems. You haven’t felt this blocking of the main thread because you’ve made simple fetch requests fetching a few objects at a time.
Since the beginning of Core Data, the framework has given developers several techniques to perform fetches in the background. As of iOS 8, Core Data has an API for performing long-running fetch requests in the background and getting a completion callback when the fetch completes.
Let’s see this new API in action. Open ViewController.swift and add the following property below venues:
var asyncFetchRequest: NSAsynchronousFetchRequest<Venue>?
There you have it. The class responsible for this asynchronous magic is aptly called NSAsynchronousFetchRequest. Don’t be fooled by its name, though. It’s not directly related to NSFetchRequest; it’s actually a subclass of NSPersistentStoreRequest.
Next, replace the contents of viewDidLoad() with the following:
override func viewDidLoad() {
super.viewDidLoad()
importJSONSeedDataIfNeeded()
// 1
let venueFetchRequest: NSFetchRequest<Venue> =
Venue.fetchRequest()
fetchRequest = venueFetchRequest
// 2
asyncFetchRequest =
NSAsynchronousFetchRequest<Venue>(
fetchRequest: venueFetchRequest) {
[unowned self] (result: NSAsynchronousFetchResult) in
guard let venues = result.finalResult else {
return
}
self.venues = venues
self.tableView.reloadData()
}
// 3
do {
guard let asyncFetchRequest = asyncFetchRequest else {
return
}
try coreDataStack.managedContext.execute(asyncFetchRequest)
// Returns immediately, cancel here if you want
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
There’s a lot you haven’t seen before, so let’s cover it step by step:
-
Notice here that an asynchronous fetch request doesn’t replace the regular fetch request. Rather, you can think of an asynchronous fetch request as a wrapper around the fetch request you already had.
-
To create an
NSAsynchronousFetchRequestyou need two things: a plain oldNSFetchRequestand a completion handler. Your fetched venues are contained inNSAsynchronousFetchResult’sfinalResultproperty. Within the completion handler, you update thevenuesproperty and reload the table view. -
Specifying the completion handler is not enough! You still have to execute the asynchronous fetch request. Once again,
CoreDataStack’smanagedContextproperty handles the heavy lifting for you. However, notice the method you use is different — this time, it’sexecute(_:)instead of the usualfetch(_:).
execute(_:) returns immediately. You don’t need to do anything with the return value since you’re going to update the table view from within the completion block. The return type is NSAsynchronousFetchResult.
Note: As an added bonus to this API, you can cancel the fetch request with
NSAsynchronousFetchResult’scancel()method.
Time to see if your asynchronous fetch delivers as promised. If everything goes well, you shouldn’t notice any difference in the user interface.
Build and run the sample app, and you should see the list of venues as before:
Hooray! You’ve mastered asynchronous fetching. The filters and sorts will also work, except they still use a plain NSFetchRequest to reload the table view.
Batch updates: no fetching required
Sometimes the only reason you fetch objects from Core Data is to change a single attribute. Then, after you make your changes, you have to commit the Core Data objects back to the persistent store and call it a day. This is the normal process you’ve been following all along.
But what if you want to update a hundred thousand records all at once? It would take a lot of time and a lot of memory to fetch all of those objects just to update one attribute. No amount of tweaking your fetch request would save your user from having to stare at a spinner for a long, long time.
Luckily, as of iOS 8 there has been new way to update Core Data objects without having to fetch anything into memory: batch updates. This new technique greatly reduces the amount of time and memory required to make those huge kinds of updates.
The new technique bypasses the NSManagedObjectContext and goes straight to the persistent store. The classic use case for batch updates is the “Mark all as read” feature in a messaging application or e-mail client. For this sample app, you’re going to do something more fun. Since you love bubble tea so much, you’re going to mark every Venue in Core Data as your favorite.
Let’s see this in practice. Open ViewController.swift and add the following to viewDidLoad() below the importJSONSeedDataIfNeeded() call:
let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate =
[#keyPath(Venue.favorite): true]
batchUpdate.affectedStores =
coreDataStack.managedContext
.persistentStoreCoordinator?.persistentStores
batchUpdate.resultType = .updatedObjectsCountResultType
do {
let batchResult =
try coreDataStack.managedContext.execute(batchUpdate)
as? NSBatchUpdateResult
print("Records updated \(String(describing: batchResult?.result))")
} catch let error as NSError {
print("Could not update \(error), \(error.userInfo)")
}
You create an instance of NSBatchUpdateRequest with the entity you want to update, Venue in this case.
Next, you set up your batch update request by setting propertiesToUpdate to a dictionary that contains the key path of the attribute you want to update, favorite, and its new value, true. Then you set affectedStores to your persistent store coordinator’s persistentStores array.
Finally, you the result type to return a count and execute your batch update request.
Build and run your sample app. If everything works properly, you’ll see the following printed to your console log:
Records updated 30
Great! You’ve surreptitiously marked every bubble tea venue in New York City as your favorite.
Now you know how to update your Core Data objects without loading them into memory. Is there another use case where you may want to bypass the managed context and change your Core Data objects directly in the persistent store?
Of course there is — batch deletion!
You shouldn’t have to to load objects into memory just to delete them, particularly if you’re handling a large number of them. As of iOS 9, you’ve had NSBatchDeleteRequest for this purpose.
As the name suggests, a batch delete request can efficiently delete a large number Core Data objects in one go.
Like NSBatchUpdateRequest, NSBatchDeleteRequest is also a subclass of NSPersistentStoreRequest. Both types of batch request behave similarly since they both operate directly on the persistent store.
Note: Since you’re sidestepping your
NSManagedObjectContext, you won’t get any validation if you use a batch update request or a batch delete request. Your changes also won’t be reflected in your managed context.Make sure you’re sanitizing and validating your data properly before using a persistent store request!
Key points
-
NSFetchRequestis a generic type. It takes a type parameter that specifies the type of objects you expect to get as the result of the fetch request. - If you expect to reuse the same type of fetch in different parts of your app, consider using the Data Model Editor to store an immutable fetch request directly in your data model.
- Use
NSFetchRequest’s count result type to efficiently compute and return counts from SQLite. - Use
NSFetchRequest’s dictionary result type to efficiently compute and return averages, sums and other common calculations from SQLite. - A fetch request uses different techniques such as using batch sizes, batch limits and faulting to limit the amount of information returned.
- Add a sort description to your fetch request to efficiently sort your fetched results.
- Fetching large amounts of information can block the main thread. Use
NSAsynchronousFetchRequestto offload some of this work to a background thread. -
NSBatchUpdateRequestandNSBatchDeleteRequestreduce the amount of time and memory required to update or delete a large number of records in Core Data.