Core Data on iOS 5 Tutorial: How To Work with Relations and Predicates

This is a tutorial where you’ll learn how to work with predicates and relationships in Core Data. It is written by iOS Tutorial Team member Cesare Rocchi, a UX designer and developer specializing in web and mobile applications. Good news – by popular request, we now have a 4th part to our Core Data tutorial […] By Cesare Rocchi.

Leave a rating/review
Save for later
Share
You are currently viewing page 5 of 6 of this article. Click here to view the first page.

Predicates: Having It Your Way

So far you have always fetched all the objects. Rather greedy, aren’t you? :]

But what if you don’t want everything? What if you want a subset, such as:

  • All the banks whose names contain a given string.
  • All the banks closed on a given date.
  • All the banks whose zip codes end with a specific digit or string of digits.
  • All the banks closed after a given date.
  • All the banks with at least one tag.

These are just a few examples of the rich variety of queries that can be made to a database. And you can create even more complex queries by using AND/OR logical operators.

Well there’s good news – you can easily do this in Core Data with something magical called a “predicate!”

A predicate is an operator whose job it is to return true or false. Whenever you have a list of objects that you want to filter, you can apply a predicate to the list.

This will apply the predicate condition (in other words, “filter criteria”) to each one. It will return a subset (possibly empty) of the original list, with only the objects that matched the condition.

NSPredator… erm I mean NSPredicate!

In Objective-C, predicates are implemented via the NSPredicate class. There is a wide range of operators that can be used with NSPredicate. Operators are special keywords that allow defining a predicate. Each predicate has a format defined as a string.

The following, for example, defines a predicate that checks for the condition “has name equal to,” where someName is a string variable containing the name to check for:

NSPredicate *pred = [NSPredicate predicateWithFormat:@"name == %@", someName];

The basic Objective-C code to use a predicate has the following format:

...
NSFetchRequest *fetchRequest = ... ;    
NSPredicate *pred = ...;    
[fetchRequest setPredicate:pred];
...

Here is a non-exhaustive list of predicate operators (a complete list is available here):

  • CONTAINS: to query for strings that contain substrings.
  • ==: equality operator.
  • BEGINSWITH: a pre-made regular expression that looks for matches at the beginning of a string.
  • MATCHES: regular expression-like search.
  • ENDSWITH: opposite of BEGINSWITH.
  • <, >: less than and greater than.

In the case of strings, it’s also possible to specify case-sensitivity. By default BEGINSWITH and the like are case sensitive. If you are not interested in the case, you can use the [c] key to specify a case-insensitive search. For example, the following looks for a string beginning with the value contained in “someName,” regardless of the case:

pred = [NSPredicate predicateWithFormat:@"name BEGINSWITH[c] %@", someName];

So if “someName” contained the value “cat,” it would match both “catatonic” and “catacombs.” Wow, those are rather dark words! I suppose it would also match words like “caterpillar” and “catnap,” for those of you with sunnier dispositions. :]

Integrating Predicates With the App

Now you’re going to build a new view controller that lets the user run searches on the database of banks. Create a new file using the Objective-C class template. This new class will be called SMSearchViewController and will extend UIViewController. And remember to create a XIB file to match the class.

Replace the contents of SMSearchViewController.h with the following:

#import <UIKit/UIKit.h>
#import "FailedBankInfo.h"

@interface SMSearchViewController : UIViewController<UITableViewDelegate, UITableViewDataSource,NSFetchedResultsControllerDelegate, UISearchBarDelegate>

@property (nonatomic,strong) NSManagedObjectContext* managedObjectContext;
@property (nonatomic,retain) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, strong) IBOutlet UISearchBar *searchBar;
@property (nonatomic, strong) IBOutlet UITableView *tView;
@property (nonatomic, strong) UILabel *noResultsLabel;

-(IBAction)closeSearch;

@end

Here you give the view controller references to a context to run the searches, a search bar, a table view and their respective protocols.

Switch to SMSearchViewController.xib and add a toolbar, a table view and a search bar, and link them to the respective outlets you defined above. The final screen should look something like this:

Switch to SMSearchViewController.m to synthesize the properties and define a helper method you’ll implement later:

@interface SMSearchViewController ()
-(void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;
@end

@implementation SMSearchViewController

@synthesize managedObjectContext;
@synthesize fetchedResultsController = _fetchedResultsController;
@synthesize searchBar,tView;
@synthesize noResultsLabel;

Add the code for closeSearch, which simply dismisses the view controller, to the end of the file:

-(IBAction)closeSearch {
    [self dismissModalViewControllerAnimated:YES];
}

In viewDidLoad, assign the delegate to the table and the search bar, and initialize the noResultsLabel:

-(void)viewDidLoad {
    [super viewDidLoad];
    self.searchBar.delegate = self;
    self.tView.delegate = self;
    self.tView.dataSource = self;
    
    noResultsLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 90, 200, 30)];
    [self.view addSubview:noResultsLabel];
    noResultsLabel.text = @"No Results";
    [noResultsLabel setHidden:YES];
}

When the view appears, display the keyboard:

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.searchBar becomeFirstResponder];
}

Once the user taps “Search” on the keyboard (after typing in a search value, of course), you run a fetch and show the results on the table view, or display the “No results” label. Add the following method to the end of the file to do that:

-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    NSError *error;
	if (![[self fetchedResultsController] performFetch:&error]) {
		NSLog(@"Error in search %@, %@", error, [error userInfo]);
	} else {
        [self.tView reloadData];
        [self.searchBar resignFirstResponder];
        [noResultsLabel setHidden:_fetchedResultsController.fetchedObjects.count > 0];
    }
}

The table view dataSource methods are pretty intuitive and fairly routine. You just display the cell as in the master view controller.

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id  sectionInfo =
    [[_fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
    
}

-(void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    FailedBankInfo *info = [_fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = info.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"%@, %@",
                                 info.city, info.state];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

Now you’re left with the core functionality for the view: the fetched results controller with a predicate. Add the following code to the end of the file:

-(NSFetchedResultsController *)fetchedResultsController {
    // Create fetch request    
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo" 
        inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
    NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"details.closeDate" ascending:NO];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
    [fetchRequest setFetchBatchSize:20];
    // Create predicate
    NSPredicate *pred = [NSPredicate predicateWithFormat:@"name CONTAINS %@", self.searchBar.text];
    [fetchRequest setPredicate:pred];
    // Create fetched results controller
    NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
        managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil]; // better to not use cache
    self.fetchedResultsController = theFetchedResultsController;
    _fetchedResultsController.delegate = self;
    return _fetchedResultsController;
}

The first part is pretty similar to what you’ve already seen: you create a request, specify an entity, and assign a batch size to it. Then you get to choose which predicate to play with. The code above uses CONTAINS. You assign the predicate to the fetch request and then create and return a fetched results controller, as usual.

The final step is, of course, to implement the search functionality in the main view :] Switch to FBCDMasterViewController.m and add the following code:

// Add at the top of the file under the imports section
#import "SMSearchViewController.h"

// Add at the bottom of the file before @end
-(void)showSearch {
    SMSearchViewController *searchViewController = [[SMSearchViewController alloc] init];
    searchViewController.managedObjectContext = managedObjectContext;
    [self.navigationController presentModalViewController:searchViewController 
                                                 animated:YES];
}

Time to test your code again!

Compile and run the application, and look for banks whose names start with the string typed into the search bar. Remember that by default the CONTAINS operator is case-sensitive. Here’s an example of this version of the app in action:

Pretty powerful stuff!

Contributors

Over 300 content creators. Join our team.