Filtering a UITableView with a UISearchBar using Core Data

Continuing the tradition of UITableView filtering articles on this blog, I thought it would be fun to do one on filtering with Core Data.

This won’t be a Core Data tutorial though, so I won’t be going in to much detail on how Core Data itself works, or how to set it up.

We’ll start off with the UITableView filtering sample from my original blog post. We’ll then create a very simple object model containing our Food class, and give it a name and a desc property.

<insert boring Core Data boilerplate code here>

Now that that’s all done, we’ll start off by populating our data model with some default foods. We can do that in our App Delegate. We want to make sure the initial data is only added once, so we’ll use a key in NSUserDefaults to keep track of whether or not we’ve added our data yet. So we’ll add some code to application:didFinishLaunchingWithOptions.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    bool hasAddedData =  [prefs boolForKey:@"HasAddedInitialData"];
    if(!hasAddedData)
    {
        [self addInitialData];
        [prefs setBool:true forKey:@"HasAddedInitialData"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }

    return YES;
}

We call the addInitialData method if we haven’t done it before. The addInitialData method looks like this:

-(void)createFoodWithName:(NSString*)name andDescription:(NSString*)description
{
    Food* food = (Food*)[NSEntityDescription insertNewObjectForEntityForName:@"Food" inManagedObjectContext:self.managedObjectContext];
    food.name = name;
    food.details = description;
}

-(void)addInitialData
{
    [self createFoodWithName:@"Steak" andDescription:@"Rare"];
    [self createFoodWithName:@"Steak" andDescription:@"Medium"];
    [self createFoodWithName:@"Salad" andDescription:@"Caesar"];
    [self createFoodWithName:@"Salad" andDescription:@"Bean"];
    [self createFoodWithName:@"Fruit" andDescription:@"Apple"];
    [self createFoodWithName:@"Potato" andDescription:@"Baked"];
    [self createFoodWithName:@"Potato" andDescription:@"Mashed"];
    [self createFoodWithName:@"Bread" andDescription:@"White"];
    [self createFoodWithName:@"Bread" andDescription:@"Brown"];
    [self createFoodWithName:@"Hot Dog" andDescription:@"Beef"];
    [self createFoodWithName:@"Hot Dog" andDescription:@"Chicken"];
    [self createFoodWithName:@"Hot Dog" andDescription:@"Veggie"];
    [self createFoodWithName:@"Pizza" andDescription:@"Pepperonni"]; 

    [self.managedObjectContext save:nil];
}

It simply populates the data model with our default foods.

Up next comes the changes to our UITableViewController. We’ll change the viewDidLoad method so that it looks like this. The primary difference between the old version and this version is that this version sets up its managedObjectContext so that it can make Core Data calls. It also calls the shiny new filter method that does the actual filtering.

- (void)viewDidLoad
{
    [super viewDidLoad];

    UITableViewFilterDemoAppDelegate* del = [UIApplication sharedApplication].delegate;
    self.managedObjectContext = del.managedObjectContext;

    searchBar.delegate = (id)self;

    [self filter:@""];
}

The filter method is where we do the real work.

-(void)filter:(NSString*)text
{
    filteredTableData = [[NSMutableArray alloc] init];

    // Create our fetch request
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

    // Define the entity we are looking for
    NSEntityDescription *entity = [NSEntityDescription
                                   entityForName:@"Food" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

    // Define how we want our entities to be sorted
    NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc]
                                        initWithKey:@"name" ascending:YES];
    NSArray* sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];

    // If we are searching for anything...
    if(text.length > 0)
    {
        // Define how we want our entities to be filtered
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(name CONTAINS[c] %@) OR (details CONTAINS[c] %@)", text, text];
        [fetchRequest setPredicate:predicate];
    }

    NSError *error;

    // Finally, perform the load
    NSArray* loadedEntities = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    filteredTableData = [[NSMutableArray alloc] initWithArray:loadedEntities];

    [self.tableView reloadData];
}

It’s a little complicated, but the comments essentially describe the flow. The important part – the actual filtering – is the NSPredicate* part. We create a predicate which says we want to find all foods with either a name or details that contains our search string. The [c] is used to indicate that the match be case insensitive.

The results of the filtering are stored in the class’s filteredTableData object.

Finally, there are a few more details we have to worry about. We’ll have to add our usual UITableView data source methods. The methods simply return the appropriate data from the filteredTableData array. We also want to set up the textDidChange searchBar delegate method so that it calls our filter method.

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

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    int rowCount = filteredTableData.count;
    return rowCount;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];

    Food* food = [filteredTableData objectAtIndex:indexPath.row];
    cell.textLabel.text = food.name;
    cell.detailTextLabel.text = food.details;

    return cell;
}

-(void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)text
{
    [self filter:text];
}

And there you have it – a UISearchBar filtering a UITableView with the power of Core Data!

Download the demo project

 

6 Comments

  1. This is one of the most detailed tutorial I have found when it comes to UISearchBar and Core Data. Thank you for taking your time!

    Reply

  2. Nice, thanks, but this way, when the search text is empty you are allocating everything in memory. Isn’t the point of core data to allocate only the necessary items (like the visible items of the table view)?

    Is that right or things are being allocated as needed?

    thanks

    Reply

    1. This appears to be the case. I’m a newbie to programming but in using this code my tableview with 400+ entries is incredibly laggy. The keyboard is borderline unusable as the phone stalls with every key press. Probably doesn’t matter too much for a small list of items.

      Wish I knew how to fix this as this was the only code that got my search to work with Core Data.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *