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

Learn how to use relationships and predicates with Core Data!

Learn how to use relationships and predicates with Core Data!

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 series! :]

In the first part of the series, you learned how to visually build a data model and show it in a table view.

In the second part of the series, you saw how to import data from an existing database to populate an application with information.

The third part of the series, you learned how to use NSFetchedResultsController to retrieve data from the object graph.

Now in Part 4, you’ll learn how to deal with predicates and relationships. You’ll start by modifying the project to allow editing objects. Then you’ll introduce relationships into the project and learn how to build specific queries by using NSPredicate.

If you’re already familiar with the basics of Core Data but just want to learn more about relationships and predicates, don’t worry you can still follow along. Just take a few moment to look over the starter project first.

The starter project will be the finished project as it stood at the end of Part 3 of the series. You can grab it here.

Now let’s continue our lovely relationship with Core Data!

Getting Started: Editing, Not Just for Writers

As a reminder, we left off in part 3 with a simple Core Data app that shows a list of banks that have failed in the US. A bit too long of a list for comfort! :]

However, the list was read-only – no editing! So before we go any further, let’s add editing into the app. This will make the app more functional, all while teaching you about relations and predicates in Core Data.

More precisely, you’ll introduce the functionality to add a bank to the list and to edit banks already stored in the database. Then you’ll make it possible to search the list for banks that meet certain criteria.

If you just downloaded the project, extract the ZIP file to a location of your choice. Open the project in Xcode.

To start with a clean slate, you’ll get rid of the procedure that imports the sqlite database. This way, our list of banks will be empty and we can add new ones with our soon-to-come editing capabilities.

So go to FBCDAppDelegate.m, go to persistentStoreCoordinator and delete the following code.

    if (![[NSFileManager defaultManager] fileExistsAtPath:[storeURL path]]) {
        NSURL *preloadURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"CoreDataTutorial2" ofType:@"sqlite"]];
        
        NSError* err = nil;
        
        if (![[NSFileManager defaultManager] copyItemAtURL:preloadURL toURL:storeURL error:&err]) {
            NSLog(@"Oops, could copy preloaded data");
        }
    }

In the same file, go to application:didFinishLaunchingWithOptions: and delete the following:

    NSManagedObjectContext *context = [self managedObjectContext];
    FailedBankInfo *failedBankInfo = [NSEntityDescription
                                      insertNewObjectForEntityForName:@"FailedBankInfo"
                                      inManagedObjectContext:context];
    failedBankInfo.name = @"Test Bank";
    failedBankInfo.city = @"Testville";
    failedBankInfo.state = @"Testland";
    FailedBankDetails *failedBankDetails = [NSEntityDescription
                                            insertNewObjectForEntityForName:@"FailedBankDetails"
                                            inManagedObjectContext:context];
    failedBankDetails.closeDate = [NSDate date];
    failedBankDetails.updateDate = [NSDate date];
    failedBankDetails.zip = [NSNumber numberWithInt:12345];
    failedBankDetails.info = failedBankInfo;
    failedBankInfo.details = failedBankDetails;
    NSError *error;
    if (![context save:&error]) {
        NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
    }
    
    // Test listing all FailedBankInfos from the store
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo"
                                              inManagedObjectContext:context];
    [fetchRequest setEntity:entity];
    NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
    for (FailedBankInfo *info in fetchedObjects) {
        NSLog(@"Name: %@", info.name);
        FailedBankDetails *details = info.details;
        NSLog(@"Zip: %@", details.zip);
    }

And with those few clicks and keystrokes, the project is clean! It no longer imports any data from a batch procedure or inserts data when the application is launched.

Build and run, and you should see an empty table view, as follows:

Now you can set about making it possible for users to add data as they see fit.

Now add two buttons to FBCDMasterViewController. One will be used to add a new bank, and the other will show the search view. In FBCDMasterViewController.m, add the following code to the bottom of viewDidLoad.

    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd 
                                                                                          target:self 
                                                                                          action:@selector(addBank)];
    
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch 
                                                                                           target:self 
                                                                                           action:@selector(showSearch)];

Let’s focus on addBank first. Add the method to the end of FBCDMasterViewController.m (but before the final @end):

-(void)addBank {
    
    FailedBankInfo *failedBankInfo = (FailedBankInfo *)[NSEntityDescription insertNewObjectForEntityForName:@"FailedBankInfo"
                                                                                     inManagedObjectContext:managedObjectContext];
    failedBankInfo.name = @"Test Bank";
    failedBankInfo.city = @"Testville";
    failedBankInfo.state = @"Testland";
    
    FailedBankDetails *failedBankDetails = [NSEntityDescription insertNewObjectForEntityForName:@"FailedBankDetails"
                                                                         inManagedObjectContext:managedObjectContext];
    failedBankDetails.closeDate = [NSDate date];
    failedBankDetails.updateDate = [NSDate date];
    failedBankDetails.zip = [NSNumber numberWithInt:123];
    failedBankDetails.info = failedBankInfo;
    failedBankInfo.details = failedBankDetails;
    
    NSError *error = nil;
    if (![managedObjectContext save:&error]) {
        NSLog(@"Error in adding a new bank %@, %@", error, [error userInfo]);
        abort();
    } 
    
}

The above code is pretty similar to the code you deleted before. You create an instance of FailedBankInfo and you populate the properties with values. One of the properties is an instance of FailedBankDetails, which you set as the “details” property.

Finally, you save the context to make sure the insertion is committed to the database. If you run the application now, you should notice that the table view gets updated correctly with the new instance without requiring a call to reloadData. How come?

This is due to the these functions, both inherited from previous versions of the project:

  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: This takes care of four possible changes to the table view: insertions, deletions, updates and moves.
  • controllerWillChangeContent: This simply “alerts” the controller of upcoming changes via the fetched results controller.

Deleting Banks

Now that you’ve got the ability to add a bank, let’s add deletion as well!

You can enable that by adding the swipe-to-delete functionality, built-in to table views. You just need to add two methods.

The first new method simply indicates which cells in the table are editable. You can either add the following code below tableView:cellForRowAtIndexPath:, or you can uncomment the pre-existing commented-out block of code for the method:

-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

The second new method, also to be added to FBCDMasterViewController.m, is tableView:commitEditingStyle:forRowAtIndexPath:. Again, there is a commented-out section of code for this, but instead of using it, replace that code with the following:

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        
        [managedObjectContext deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
        
        NSError *error = nil;
        if (![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }   
}

As before, there’s no need to refresh the table view! It’s all handled by the fetched results controller, which notifies the controller that changes have occured.

Build and run, add a few banks, and swipe one of the cells to show the delete button. Tap the button, and you’ll see the record deleted and the table view refreshed!

Contributors

Over 300 content creators. Join our team.