Learning Core Data for iOS (2014)
12. Search
The measure of intelligence is the ability to change.
Albert Einstein
In Chapter 11, “Background Processing,” the execution of background tasks such as save, import, and thumbnail creation was demonstrated. This chapter will implement search on the PrepareTVC table view of the Prepare tab. Search results will have the same sectioning and sort order as the PrepareTVC table view so that it looks like search results are filtered in place. This functionality will be added in such a way that it will be easy to enable search on any table view, with only minor updates required. This flexibility will be achieved by implementing as much of the search functionality into the underlying CoreDataTVC as possible. The performance implications of the way search predicates are formed will also be discussed, along with recommendations for optimizing search for large data sets.
To add search to a table view, you’ll need to use a UISearchDisplayController. This class contains a table view that is used to display search results. The UITableView data source methods implemented in CoreDataTVC will need to be updated to handle this additional table view. When a user taps in the search bar, the search display controller’s table view will become visible. As the user types in search text, he or she will effectively be specifying the predicate that is used to filter the search fetch request. This fetch request will be given to a new fetched results controller put in place specifically for search results. The NSFetchedResultsController delegate methods in CoreDataTVC will also need to be updated to handle this additional fetched results controller.
Note
To continue building the sample application, you’ll need to have added the previous chapter’s code to Grocery Dude. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LearningCoreData/GroceryDude-AfterChapter11.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to click Product > Clean. This practice ensures there’s no residual cache from previous projects using the same name.
Updating CoreDataTVC
By updating CoreDataTVC to support search, all table views in Grocery Dude will inherit the ability to easily implement search. To add search support to CoreDataTVC, its header will need to be updated to adopt the UISearchBarDelegate and UISearchDisplayDelegate protocols. Listing 12.1 shows how the updated CoreDataTVC header will look once it adopts these search protocols.
Listing 12.1 CoreDataTVC.h
#import <UIKit/UIKit.h>
#import "CoreDataHelper.h"
@interface CoreDataTVC : UITableViewController
<NSFetchedResultsControllerDelegate, UISearchBarDelegate, UISearchDisplayDelegate>
@property (strong, nonatomic) NSFetchedResultsController *frc;
- (void)performFetch;
@end
Update Grocery Dude as follows to adopt the search protocols:
1. Update CoreDataTVC.h to adopt the UISearchBarDelegate and UISearchDisplayDelegate protocols by replacing all the code in CoreDataTVC.h with the code from Listing 12.1.
The next step will be to add two new properties to CoreDataTVC. The first property, searchFRC, will be a fetched results controller. It will efficiently manage search results. When a user types in search text, the searchFRC will be remade with a new predicate formed from the search text. The second property, searchDC, will be a search display controller for displaying search results. Listing 12.2 shows the two new properties.
Listing 12.2 CoreDataTVC.h: searchFRC and searchDC
@property (strong, nonatomic) NSFetchedResultsController *searchFRC;
@property (strong, nonatomic) UISearchDisplayController *searchDC;
Update Grocery Dude as follows to add two new properties for search:
1. Add the two properties from Listing 12.2 to CoreDataTVC.h beneath the existing frc property.
The CoreDataTVC now needs to be updated to support both self.frc and self.searchFRC. This means that the UITableView data source and NSFetchedResultsController delegate methods need updating. First, however, two new methods will be added to help determine what table view is used with what fetched results controller, and vice versa. Listing 12.3 shows the code involved, which uses a ternary operator to make these determinations.
Listing 12.3 CoreDataTVC.m: GENERAL
#pragma mark - GENERAL
- (NSFetchedResultsController*)frcFromTV:(UITableView*)tableView {
/*
If the given tableView is self.tableView return self.frc,
otherwise self.searchFRC
*/
return (tableView == self.tableView) ? self.frc : self.searchFRC;
}
- (UITableView*)TVFromFRC:(NSFetchedResultsController*)frc {
/*
If the given fetched results controller is self.frc return self.tableView,
otherwise self.searchDC.searchResultsTableView
*/
return (frc == self.frc) ? self.tableView : self.searchDC.searchResultsTableView;
}
Update Grocery Dude as follows to implement two new methods in a GENERAL section:
1. Add the code from Listing 12.3 to the bottom of CoreDataTVC.m before @end.
2. Add the following code to the bottom of CoreDataTVC.h before @end.
- (NSFetchedResultsController*)frcFromTV:(UITableView*)tableView;
- (UITableView*)TVFromFRC:(NSFetchedResultsController*)frc;
Listing 12.4 shows updated UITableView data source methods that leverage the new helper methods in the GENERAL section.
Listing 12.4 CoreDataTVC.m: DATASOURCE: UITableView
#pragma mark - DATASOURCE: UITableView
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[[[self frcFromTV:tableView]sections]
objectAtIndex:section] numberOfObjects];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[[self frcFromTV:tableView] sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView
sectionForSectionIndexTitle:(NSString *)title
atIndex:(NSInteger)index {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[self frcFromTV:tableView]
sectionForSectionIndexTitle:title atIndex:index];
}
- (NSString *)tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[[[self frcFromTV:tableView] sections] objectAtIndex:section] name];
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[self frcFromTV:tableView] sectionIndexTitles];
}
Update Grocery Dude as follows to add support for the searchFRC:
1. Replace the entire DATASOURCE: UITableView section of CoreDataTVC.m with the code from Listing 12.4 (Tip: With the cursor inside CoreDataTVC.m, click Editor > Code Folding > Fold Methods & Functions to get a better view of class sections).
Search results will be displayed in a table view that is a property of searchDC. This property will be accessed via searchDC.searchResultsTableView. For it to be populated with search results, the fetched results controller delegate methods of CoreDataTVC.m need to be updated. The helper methods from the GENERAL section will be leveraged again. The code involved is shown in Listing 12.5.
Listing 12.5 CoreDataTVC.m: DELEGATE: NSFetchedResultsController
#pragma mark - DELEGATE: NSFetchedResultsController
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
[[self TVFromFRC:controller] beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
[[self TVFromFRC:controller] endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
switch(type) {
case NSFetchedResultsChangeInsert:
[[self TVFromFRC:controller]
insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self TVFromFRC:controller]
deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
switch(type) {
case NSFetchedResultsChangeInsert:
[[self TVFromFRC:controller]
insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[[self TVFromFRC:controller]
deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
if (!newIndexPath) {
[[self TVFromFRC:controller]
reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
else {
[[self TVFromFRC:controller]
deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
[[self TVFromFRC:controller]
insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
break;
case NSFetchedResultsChangeMove:
[[self TVFromFRC:controller]
deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
[[self TVFromFRC:controller]
insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
Update Grocery Dude as follows to add support for the table view in searchDC:
1. Replace the DELEGATE: NSFetchedResultsController section of CoreDataTVC.m with the code from Listing 12.5.
The next step is to add a UISearchDisplayController delegate method to CoreDataTVC.m. This method will set the search fetched results controller and delegate to nil when the search ends. The code involved is shown in Listing 12.6.
Listing 12.6 CoreDataTVC.m: DELEGATE: UISearchDisplayController
#pragma mark - DELEGATE: UISearchDisplayController
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
self.searchFRC.delegate = nil;
self.searchFRC = nil;
}
Update Grocery Dude as follows to implement the searchDisplayControllerDidEndSearch method:
1. Add the code from Listing 12.6 to the bottom of CoreDataTVC.m before @end.
The next step is to update CoreDataTVC to enable it to reload the searchFRC each time the search text changes. A new method will be implemented that takes a predicate and uses it to fetch objects of a given entity in a given context. Sorting and sectioning information is also passed so the search results can be arranged appropriately. Listing 12.7 shows this new method header.
Listing 12.7 CoreDataTVC.h: reloadSearchFRCForPredicate
- (void)reloadSearchFRCForPredicate:(NSPredicate*)predicate
withEntity:(NSString*)entity
inContext:(NSManagedObjectContext*)context
withSortDescriptors:(NSArray*)sortDescriptors
withSectionNameKeyPath:(NSString*)sectionNameKeyPath;
Update Grocery Dude as follows to add the reloadSearchFRCForPredicate method header:
1. Add the code from Listing 12.7 to the bottom of CoreDataTVC.h before @end.
The implementation of the reloadSearchFRCForPredicate method should contain code that is quite familiar by now. All this method does is create a fetch request using the given variables and performs a fetch. The code involved is shown in Listing 12.8.
Listing 12.8 CoreDataTVC.m: reloadSearchFRCForPredicate
#pragma mark - SEARCH
- (void)reloadSearchFRCForPredicate:(NSPredicate*)predicate
withEntity:(NSString*)entity
inContext:(NSManagedObjectContext*)context
withSortDescriptors:(NSArray*)sortDescriptors
withSectionNameKeyPath:(NSString*)sectionNameKeyPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:entity];
request.sortDescriptors = sortDescriptors;
request.predicate = predicate;
request.fetchBatchSize = 15;
self.searchFRC =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:sectionNameKeyPath
cacheName:nil];
self.searchFRC.delegate = self;
[self.searchFRC.managedObjectContext performBlockAndWait:^{
NSError *error;
if (![self.searchFRC performFetch:&error]) {
NSLog(@"SEARCH FETCH ERROR: %@", error);
}
}];
}
Update Grocery Dude as follows to implement the reloadSearchFRCForPredicate method:
1. Add the code from Listing 12.8 to the bottom of CoreDataTVC.m before @end.
The final addition to CoreDataTVC is a new method called configureSearch, which will be used by its subclasses to enable search. This new method will programmatically add a UISearchBar to the header of the respective table view. In addition, appropriate search bar delegate and data source information will be configured. Listing 12.9 shows the code involved.
Listing 12.9 CoreDataTVC.m: configureSearch
- (void)configureSearch {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
UISearchBar *searchBar =
[[UISearchBar alloc] initWithFrame:
CGRectMake(0, 0, self.tableView.frame.size.width, 44.0)];
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
self.tableView.tableHeaderView = searchBar;
self.searchDC =
[[UISearchDisplayController alloc] initWithSearchBar:searchBar
contentsController:self];
self.searchDC.delegate = self;
self.searchDC.searchResultsDataSource = self;
self.searchDC.searchResultsDelegate = self;
}
Update Grocery Dude as follows to implement the configureSearch method:
1. Add the code from Listing 12.9 to the bottom of the SEARCH section of CoreDataTVC.m before @end.
2. Add the following code to the bottom of CoreDataTVC.h before @end:
-(void)configureSearch;
The fundamental code is now in place to enable CoreDataTVC subclasses to implement search functionality.
Updating PrepareTVC
Being a CoreDataTVC subclass, most of the legwork in configuring PrepareTVC for search has already been done. The remaining configuration will be specific to the data the table view displays. A UISearchDisplayController delegate method will also be implemented in PrepareTVC. This method will be called every time the user types text into the search bar. Provided it has a length greater than zero, a predicate will be created from the given search string. This predicate will be given to the reloadSearchFRCForPredicate method so it can reload searchFRC. The section name key path and sort descriptors provided should match those used to populate the PrepareTVC. Listing 12.10 shows the code involved.
Listing 12.10 PrepareTVC.m: SEARCH
#pragma mark - SEARCH
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchString:(NSString *)searchString {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if (searchString.length > 0) {
NSLog(@"--> Searching for '%@'", searchString);
NSPredicate *predicate =
[NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", searchString];
NSArray *sortDescriptors =
[NSArray arrayWithObjects:
[NSSortDescriptor sortDescriptorWithKey:@"locationAtHome.storedIn"
ascending:YES],
[NSSortDescriptor sortDescriptorWithKey:@"name"
ascending:YES], nil];
CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
[self reloadSearchFRCForPredicate:predicate
withEntity:@"Item"
inContext:cdh.context
withSortDescriptors:sortDescriptors
withSectionNameKeyPath:@"locationAtHome.storedIn"];
} else {
return NO;
}
return YES;
}
Update Grocery Dude as follows to configure search in PrepareTVC:
1. Add the code from Listing 12.10 to the bottom of PrepareTVC.m before @end.
2. Add [self configureSearch]; to the bottom of the viewDidLoad method of PrepareTVC.m.
The predicates configured in shouldReloadTableForSearchString are the heart of search for a table view. This predicate configuration is where you will make the most changes when adapting search to your own applications. Here are some key pointers to keep in mind when configuring search:
When using a compound predicate (two or more predicates together), put the predicate that filters out the most results first. The order in which the predicates are specified can have a big difference on query execution time.
When using a compound predicate, put the predicate that filters out dates or numbers ahead of predicates filtering text. This is secondary to the previous point.
When working with a large data set with thousands of rows, consider adding a pre-processed normalized string attribute for the entity being searched. A normalized string would be completely lowercase and contain no characters like “á.” This allows you to avoid the need for a case- and diacritic-insensitive predicate, which will speed up the search results. Case and diacritic insensitivity is specified in a predicate using [cd].
If you ran the application now and tried to search for items on the Prepare tab, it would crash because the cellForRowAtIndexPath is not configured to support the searchFRC. An updated method is shown in Listing 12.11. Updates are in bold.
Listing 12.11 PrepareTVC.m: cellForRowAtIndexPath
- (UITableViewCell*)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
static NSString *cellIdentifier = @"Item Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
reuseIdentifier:cellIdentifier];
}
cell.accessoryType = UITableViewCellAccessoryDetailButton;
Item *item = [[self frcFromTV:tableView] objectAtIndexPath:indexPath];
NSMutableString *title = [NSMutableString stringWithFormat:@"%@%@ %@",
item.quantity, item.unit.name, item.name];
[title replaceOccurrencesOfString:@"(null)"
withString:@""
options:0
range:NSMakeRange(0, [title length])];
cell.textLabel.text = title;
// make selected items orange
if ([item.listed boolValue]) {
[cell.textLabel setFont:[UIFont fontWithName:@"Helvetica Neue" size:18]];
[cell.textLabel setTextColor:[UIColor orangeColor]];
}
else {
[cell.textLabel setFont:[UIFont fontWithName:@"Helvetica Neue" size:16]];
[cell.textLabel setTextColor:[UIColor grayColor]];
}
cell.imageView.image = [UIImage imageWithData:item.thumbnail];
return cell;
}
The cellForRowAtIndexPath method has been updated to use dequeueReusableCellWithIdentifier instead of dequeueReusableCellWithIdentifier:forIndexPath because passing along the indexPath to the search results table view causes a crash. In addition, the item that’s loaded in the cell will now be dependent on what table view this method is servicing.
Update Grocery Dude as follows to add searchFRC support to cellForRowAtIndexPath:
1. Replace the cellForRowAtIndexPath method of PrepareTVC.m with the code from Listing 12.11.
Delete and rerun the application, and then search for “baby.” The expected result is shown in Figure 12.1.
Figure 12.1 Search
Search is functioning yet the commitEditingStyle, didSelectRowAtIndexPath, and accessoryButtonTappedForRowWithIndexPath methods of PrepareTVC all need updating to support the searchFRC. An updated commitEditingStyle method is shown in Listing 12.12.
Listing 12.12 PrepareTVC.m: commitEditingStyle
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSFetchedResultsController *frc = [self frcFromTV:tableView];
Item *deleteTarget = [frc objectAtIndexPath:indexPath];
[frc.managedObjectContext deleteObject:deleteTarget];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
}
CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
[cdh backgroundSaveContext];
}
Update Grocery Dude as follows to add further searchFRC support:
1. Replace the commitEditingStyle method of PrepareTVC.m with the code from Listing 12.12.
An updated didSelectRowAtIndexPath method is shown in Listing 12.13.
Listing 12.13 PrepareTVC.m: didSelectRowAtIndexPath
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
NSFetchedResultsController *frc = [self frcFromTV:tableView];
NSManagedObjectID *itemid = [[frc objectAtIndexPath:indexPath] objectID];
Item *item =
(Item*)[frc.managedObjectContext existingObjectWithID:itemid error:nil];
if ([item.listed boolValue]) {
item.listed = [NSNumber numberWithBool:NO];
}
else {
item.listed = [NSNumber numberWithBool:YES];
item.collected = [NSNumber numberWithBool:NO];
}
CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
[cdh backgroundSaveContext];
}
Update Grocery Dude as follows to add further searchFRC support:
1. Replace the didSelectRowAtIndexPath method of PrepareTVC.m with the code from Listing 12.13.
An updated accessoryButtonTappedForRowWithIndexPath method is shown in Listing 12.14.
Listing 12.14 PrepareTVC.m: accessoryButtonTappedForRowWithIndexPath
- (void)tableView:(UITableView *)tableView
accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
ItemVC *itemVC =
[self.storyboard instantiateViewControllerWithIdentifier:@"ItemVC"];
itemVC.selectedItemID =
[[[self frcFromTV:tableView] objectAtIndexPath:indexPath] objectID];
[self.navigationController pushViewController:itemVC animated:YES];
}
Update Grocery Dude as follows to add further searchFRC support:
1. Replace the accessoryButtonTappedForRowWithIndexPath method of PrepareTVC.m with the code from Listing 12.14.
Summary
You’ve now been shown how to add search to an application in such a way that it will be easy to propagate it to any table view with ease. As a part of implementing search, ternary operators were introduced as a technique for handling multiple table views and fetched result controllers without bloating the code. Take care when you implement search in your own applications to understand the expected size of the data sets where search will be required. When it is expected that a large data set will be searched, consider adding special search attributes to your entities with pre-processed normalized strings. This will ensure search is seamless and responsive for your users. If you have advanced requirements for search, you may wish to investigate Apple’s Search Kit framework.
Exercises
Why not build on what you’ve learned by experimenting?
1. Alter the search predicate used in the shouldReloadTableForSearchString method of PrepareTVC with the following options. Search for the letter “j” as you compare the query execution times and search results between the different predicates:
[NSPredicate predicateWithFormat:
@"name beginswith[cd] %@ OR name endswith[cd]%@"
, searchString, searchString];
[NSPredicate predicateWithFormat:@"name contains %@", searchString];
[NSPredicate predicateWithFormat:@"name like[cd] %@", [[[NSString
stringWithFormat:@"*"] stringByAppendingString:searchString]
stringByAppendingString:@"*"]];
2. Update ShopTVC to implement search using the same approach used to add search to PrepareTVC. Don’t forget to update both the key path and sort descriptor to locationAtShop.aisle when adding the shouldReloadTableForSearchString method to ShopTVC.m. To prevent crashes, you’ll also need to update the cellForRowAtIndexPath method in the same way the same method in PrepareTVC.m has been updated.
3. Modify the search predicate in ShopTVC.m so that only listed items are searched.
Once you’ve completed the exercises, disable SQL debug mode. Also, disable debug in every class except CoreDataHelper.m and AppDelegate.m. To disable debug, search for #define debug 1 and replace it with #define debug 0.
The code to enable search in ShopTVC is in the sample project and is commented out for reference.