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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Core Data on iOS 5 Tutorial: How To Work with Relations and Predicates
45 mins
- Getting Started: Editing, Not Just for Writers
- Deleting Banks
- Editing Banks
- Relationships, But Not the Dating Kind
- Adding a Many-to-Many Relationship
- Tag, You’re It!
- Tagging Like a Fool
- Predicates: Having It Your Way
- NSPredator… erm I mean NSPredicate!
- Integrating Predicates With the App
- More Fun with Predicates
- Where To Go From Here?
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!