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 4 of 6 of this article. Click here to view the first page.

Tag, You’re It!

This new view controller will facilitate the creation of new tags and associating them to a bank details object. Create a new class that extends UITableViewController and name it SMTagListViewController by right-clicking the root of the project and selecting New File…\iOS\Cocoa Touch\Objective-C class. Remember to check the box to create the accompanying XIB file.

Replace the contents of SMTagListViewController.h with the following:

#import <UIKit/UIKit.h>
#import "FailedBankDetails.h"
#import "Tag.h"

@interface SMTagListViewController : UITableViewController <UIAlertViewDelegate>

@property (nonatomic, strong) FailedBankDetails *bankDetails;
@property (nonatomic, strong) NSMutableSet *pickedTags;
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;

-(id)initWithBankDetails:(FailedBankDetails *)details;

@end

You import two needed classes, mark the view controller as implementing the UIAlertViewDelegate, and you add three properties: the bank details that refer to the previous screen, a set to collect the picked tags for the current details, and a results controller to fetch the whole list of tags. Finally, you add a method to initialize the component with an instance of details.

At the top of SMTagListViewController.m (below the @implementation line), synthesize the properties and implement initWithBankDetails:

@synthesize bankDetails = _bankDetails;
@synthesize pickedTags;
@synthesize fetchedResultsController = _fetchedResultsController;

-(id)initWithBankDetails:(FailedBankDetails *)details {
    if (self = [super init]) {
        _bankDetails = details;
    }
    return self;
}

The fetched results controller is defined to load all the instances of tags from the context. Add the code for it as follows to the end of the file (but before the final @end):

-(NSFetchedResultsController *)fetchedResultsController {
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }        
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];    
    
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Tag" 
        inManagedObjectContext:self.bankDetails.managedObjectContext];    
    [fetchRequest setEntity:entity];
    
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" 
        ascending:NO];
    NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];    
    [fetchRequest setSortDescriptors:sortDescriptors];
    
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] 
        initWithFetchRequest:fetchRequest managedObjectContext:self.bankDetails.managedObjectContext 
        sectionNameKeyPath:nil cacheName:nil];
    self.fetchedResultsController = aFetchedResultsController;
	NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
	    NSLog(@"Core data error %@, %@", error, [error userInfo]);
	    abort();
	}    
    
    return _fetchedResultsController;
}

The above is pretty similar to previously defined fetched results controllers – it’s just for a different entity.

Replace the existing viewDidLoad with the following:

-(void)viewDidLoad {
    [super viewDidLoad];
    self.pickedTags = [[NSMutableSet alloc] init];
    // Retrieve all tags
    NSError *error;    
    if (![self.fetchedResultsController performFetch:&error]) {
	    NSLog(@"Error in tag retrieval %@, %@", error, [error userInfo]);
	    abort();
	}
    // Each tag attached to the details is included in the array
    NSSet *tags = self.bankDetails.tags;
    for (Tag *tag in tags) {    
        [pickedTags addObject:tag];   
    } 
    // setting up add button
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd 
        target:self action:@selector(addTag)];
}

Here you run a fetch operation and populate the set of pickedTags that are attached to the instance of bankDetails. You need this to show a tag as picked (by means of a tick) in the table view. You also set up a navigation item to add new tags.

Add the following below viewDidLoad:

-(void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated]; 
    self.bankDetails.tags = pickedTags;
    NSError *error = nil;
    if (![self.bankDetails.managedObjectContext save:&error]) {        
        NSLog(@"Error in saving tags %@, %@", error, [error userInfo]);
        abort();
    }         
}

When the view is closed, you save the set of picked tags by setting the tags property of bankDetails.

Now add the following code to the end of the file:

-(void)addTag {
    UIAlertView *newTagAlert = [[UIAlertView alloc] initWithTitle:@"New tag" 
        message:@"Insert new tag name" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Save", nil];
    newTagAlert.alertViewStyle = UIAlertViewStylePlainTextInput;
    [newTagAlert show];
}

To add a new tag, you use an alert view with an input text field. The code above will display an alert asking the user to insert a new tag:

To handle all actions for the alert view, add the following delegate method to the end of SMTagListViewController.m:

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == 0) {
        NSLog(@"cancel");
    } else {
        NSString *tagName = [[alertView textFieldAtIndex:0] text];
        Tag *tag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag"
            inManagedObjectContext:self.bankDetails.managedObjectContext];
        tag.name = tagName;
        NSError *error = nil;
        if (![tag.managedObjectContext save:&error]) {        
            NSLog(@"Core data error %@, %@", error, [error userInfo]);
            abort();
        } 
        [self.fetchedResultsController performFetch:&error];
        [self.tableView reloadData];
    }
}

You ignore a tap on the cancel button whereas you save the new tag if “OK” is tapped. In such a case, instead of implementing the change protocols to the table, you fetch the result again and reload the table view for the sake of simplicity.

Next replace the placeholders for numberOfSectionsInTableView and tableView:numberOfRowsInSection with the following:

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

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}

This is pretty straightforward – there is only one section, and the number of rows is calculated according to the results controller.

Next, modify tableView:cellForRowAtIndexPath: as follows:

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"TagCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
             reuseIdentifier:CellIdentifier];
    }    
    cell.accessoryType = UITableViewCellAccessoryNone;
    Tag *tag = (Tag *)[self.fetchedResultsController objectAtIndexPath:indexPath];
    if ([pickedTags containsObject:tag]) {        
        cell.accessoryType = UITableViewCellAccessoryCheckmark;        
    }
    cell.textLabel.text = tag.name;    
    return cell; 
}

This shows a checkmark if a tag belongs to the pickedTags set.

Finally, replace tableView:didSelectRowAtIndexPath with the following:

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    Tag *tag = (Tag *)[self.fetchedResultsController objectAtIndexPath:indexPath];
    UITableViewCell * cell = [self.tableView  cellForRowAtIndexPath:indexPath];
    [cell setSelected:NO animated:YES];                    
    if ([pickedTags containsObject:tag]) { 
        [pickedTags removeObject:tag];
        cell.accessoryType = UITableViewCellAccessoryNone;   
    } else {    
        [pickedTags addObject:tag];
        cell.accessoryType = UITableViewCellAccessoryCheckmark;     
    }    
}

This makes it so that when a cell is tapped, the corresponding tag is added to or removed from the set, and the cell updated accordingly.

Tagging Like a Fool

Take a deep breath – you’re almost there! :]

Make the following modifications to SMBankDetailViewController.m:

// Add import at top of file
#import "SMTagListViewController.h"

// Add the following to the end of viewDidLoad
// 4 - setting interaction on tag label
self.tagsLabel.userInteractionEnabled = YES;
UITapGestureRecognizer *tagsTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self 
    action:@selector(tagsTapped)];
[self.tagsLabel addGestureRecognizer:tagsTapRecognizer];

This adds a tap gesture recognizer so that you can get a callback when the user taps the tags label on the edit view.

Next implement this callback:

-(void)tagsTapped {
    SMTagListViewController *tagPicker = [[SMTagListViewController alloc] initWithBankDetails:self.bankInfo.details];
    [self.navigationController pushViewController:tagPicker 
                                         animated:YES];
}

So when the tags label gets tapped, we present the SMTagListViewController we just made.

viewWillAppear: needs to be tweaked a bit to display tags correctly. At the bottom of the method implementation, add this code:

NSSet *tags = self.bankInfo.details.tags;
NSMutableArray *tagNamesArray = [[NSMutableArray alloc] initWithCapacity:tags.count];
for (Tag *tag in tags) {
    [tagNamesArray addObject:tag.name];
}
self.tagsLabel.text = [tagNamesArray componentsJoinedByString:@","];

This just makes a string of all of our tags separated by commas so we can display it.

As a final touch, make the label backgrounds gray to show their tappable area. Add this to the end of viewDidLoad:

    self.tagsLabel.backgroundColor = self.dateLabel.backgroundColor = [UIColor lightGrayColor];

You’re done! Build and run your application and test it. Try the following:

  1. Add a new bank record.
  2. Tap it.
  3. Change its values.
  4. Tap the tags label (will be empty the first time).
  5. Add the tags you like.
  6. Tap a few to associate them to the details object.
  7. Tap the back button to verify that the details are correctly updated.

Note: At this point, if haven’t deleted the previous instance of your app as mentioned earlier, you might have a crash when you try to run it, with an error message saying, “The model used to open the store is incompatible with the one used to create the store.”

This happens because you changed the Core Data model since you last ran the app. You would need to delete the existing instance of the app on the simulator (or the device) and then compile and run your project. Everything should work fine at that point.

Note: At this point, if haven’t deleted the previous instance of your app as mentioned earlier, you might have a crash when you try to run it, with an error message saying, “The model used to open the store is incompatible with the one used to create the store.”

This happens because you changed the Core Data model since you last ran the app. You would need to delete the existing instance of the app on the simulator (or the device) and then compile and run your project. Everything should work fine at that point.

Contributors

Over 300 content creators. Join our team.