iPhone App Development Tutorial – Core Data part 2 – One to Many Relationship

In this tutorial we will look at creating a one to many relationship in core data. It’s really very easy and not that much different than creating the one to one relationship. The difference comes in how the relationship allows the entities to interact with each other. In part one we had two entities, Fruit and Source. These two entities had a one to one relationship to each other. For each Fruit there was only one Source, and the inverse was true as well, for each Source there was only one Fruit. Now we are going to create two new entities, Artist and Album. Each artist can have multiple albums, but each album can have only one artist. We’ll just pretend that no artists ever collaborate on albums together for the sake of this tutorial :)

We’re going to use the app that I / we created in the Core Data duplicate entities tutorial. In that tutorial we set up an app that did not allow us to enter the same Artist twice. It seems like a good app to start with, only I’m even going to take care of a few more things for us To save time. I’ve set up the second tab with a UITableView and hooked it up with the view controller. This table view will hold the the list of artists that have been saved in the app. I’ve also created a new view with view controllers that will be used to enter Album information for an artist.

Screen Shot

What is going to happen is when we go to the second tab a search will return all Artists saved and display them in the UITableView. We can then click on an Artist and go to the enter Album informations view. The Artist object will be passed to this new view so that when we enter info for an album and save it, it will be saved with a relationship to the artist we selected. In this way we can create many albums for an artist, but an album can only have one artist. Thus the one to many relationship.

Here’s the code we’ll start with.

Let’s get going.

1.) If you run the app right now you can see the second tab with a hard coded string populating each table view cell.

Select An Artist Table Hardcoded

And though there’s no way to get to the new view yet, here is what it looks like.

Enter Album Info View

So that’s what we are starting with.

2.) The first thing we are going to do is a search in SecondViewController and populate our table with the Artists returned. Open up SecondViewController.h and add an NSMutableArray to hold our Artists and declare an ivar for NSMananagedObjectContext.

#import 

@interface SecondViewController : UIViewController 
{
    NSMutableArray *artistsArray;
}

@property (nonatomic, retain) IBOutlet UITableView *artistTable;
@property (nonatomic, retain) NSManagedObjectContext        *managedObjectContext;

@end

Then go to the implementation file and synthesize the managedObjectContext ivar.

#import "SecondViewController.h"

@implementation SecondViewController

@synthesize artistTable;
@synthesize managedObjectContext;

3.) Go to app delegate header file and import SecondViewController, then declare an ivar of it and

#import 
#import "FirstViewController.h"
#import "SecondViewController.h"

@interface CoreDataTutorial5AppDelegate : NSObject  
{
    
@private
    NSManagedObjectContext *managedObjectContext;
    NSManagedObjectModel *managedObjectModel;
    NSPersistentStoreCoordinator *persistentStoreCoordinator;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UITabBarController *tabBarController;

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

@property (nonatomic, retain) IBOutlet FirstViewController *firstViewController;
@property (nonatomic, retain) IBOutlet SecondViewController *secondViewController;

- (NSURL *)applicationDocumentsDirectory;
- (void)saveContext;

@end

finally synthesize it in the app delegate implementation file.

#import "CoreDataTutorial5AppDelegate.h"

@implementation CoreDataTutorial5AppDelegate

@synthesize window=_window;
@synthesize tabBarController=_tabBarController;
@synthesize firstViewController, secondViewController;

Then open MainWindow.xib and connect the secondViewController ivar to the SecondViewController under Tab Bar Controller.

Now that they are connected let’s set the managed object context in secondViewController. Do this in the app delegate implementation file application:didFinishLaunchingWithOptions method.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.firstViewController.managedObjectContext = self.managedObjectContext;
    self.secondViewController.managedObjectContext = self.managedObjectContext;
    self.window.rootViewController = self.tabBarController;
    [self.window makeKeyAndVisible];
    return YES;
}

4.) We’ve now passed the managedObjectContext to SecondViewController and we can implement the search method. We’re going to implement the viewWillAppear method and do our search there.

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"viewWillAppear");
    NSError *error;
    
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Artist" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
    NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    artistsArray = [[NSMutableArray alloc] initWithArray:fetchedObjects];
    
    [fetchRequest release];
    [artistTable reloadData];

}

Then we’ll use our array to set the number of rows in the section.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [artistsArray count];
}

And finally we’ll populate our table view cells with the Artists name. But first import the Artist object.

#import "SecondViewController.h"
#import "Artist.h"

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
    
    // Configure the cell...
    Artist *artist = [artistsArray objectAtIndex:indexPath.row];
    cell.textLabel.text = artist.artistName;
    return cell;
}

Run it and make sure everything is working up to this point.

Select an Artist Table

5.) Now that we have retrieved the Artists and are displaying them, let’s add the new view and pass the Artist to it when selecting a cell in the table view. We’ll need to import EnterAlbumInfoViewController to the SecondViewController header file and declare an ivar of it.

#import 
#import "EnterAlbumInfoViewController.h"

@interface SecondViewController : UIViewController 
{
    NSMutableArray *artistsArray;
}

@property (nonatomic, retain) IBOutlet UITableView *artistTable;
@property (nonatomic, retain) NSManagedObjectContext        *managedObjectContext;

@property (nonatomic, retain) EnterAlbumInfoViewController *enterAlbumInfoViewController;

@end

Then synthesize it in the implementation file.

#import "SecondViewController.h"
#import "Artist.h"

@implementation SecondViewController

@synthesize artistTable;
@synthesize managedObjectContext;
@synthesize enterAlbumInfoViewController;

Now go to the didSelectRowAtIndexPath method and present the new view modally, at the same time we will pass the Artist to it along with the managedObjectContext.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.enterAlbumInfoViewController == nil) 
    {
        EnterAlbumInfoViewController *temp = [[EnterAlbumInfoViewController alloc] initWithNibName:@"EnterAlbumInfoViewController" bundle:[NSBundle mainBundle]];
        self.enterAlbumInfoViewController = temp;
        [temp release];
    }
    
    Artist *artist = [artistsArray objectAtIndex:indexPath.row];
    self.enterAlbumInfoViewController.artistNameString = artist.artistName;
    self.enterAlbumInfoViewController.artist = artist;
    self.enterAlbumInfoViewController.managedObjectContext = self.managedObjectContext;
    
    [self presentModalViewController:self.enterAlbumInfoViewController animated:YES];
}

I forgot to implement a method for the cancel button so let’s do that real quick. This goes in EnterAlbumInfoViewController.

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

6.) Before we go any further we need to add the Album Entity to our Data Model. Open up the visual data model editor by clicking on DataModel.xcdatamodel. Add an Entity named Album with three attributes. Here’s how it should look when done.

Album Entity

Now we need to create the relationships. Select Artist and add a relationship to Album named album. This relationship will be a To-Many Relationship so check that box. This means each artist can have many albums. Also make this Optional since an artist doesn’t need to have an album when we create the artist.

album relationship

Then select Album and create the inverse. This time uncheck the optional checkbox and make sure the To-Many Relationship box is not checked. Each Album will have just one artist.

artist relationship

When done create the Album NSManagedObject subclass, and recreate the Artist object. You’ll also need to change the name of the sqlite db on the back end of the Core Data. I just changed the name in this line of the persistentStoreCoordinator method of the app delegate.

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataTutorial63.sqlite"];

After doing this you’ll need to re-enter the Artist info.

7.) Now we should be ready to save the Album info. Implement the saveAlbumInfo in EnterAlbumInfoViewController. You’ll need to import “Album.h” too.

- (IBAction)saveAlbumInfo
{
    NSLog(@"saveAlbumInfo");
    
    
    Album *album = (Album *)[NSEntityDescription insertNewObjectForEntityForName:@"Album" inManagedObjectContext:managedObjectContext];
    album.albumName = self.albumNameTextField.text;
    album.albumReleaseDate = self.albumReleaseDateTextField.text;
    album.albumGenre = self.albumGenreTextField.text;
    album.artist = self.artist;
        
    NSError *error;
    
    // here's where the actual save happens, and if it doesn't we print something out to the console
    if (![managedObjectContext save:&error])
    {
        NSLog(@"Problem saving: %@", [error localizedDescription]);
    }
    
    // **** log objects currently in database ****
    // create fetch object, this object fetch's the objects out of the database
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Album" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
    NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    
    for (NSManagedObject *info in fetchedObjects)
    {
        NSLog(@"Album name: %@", [info valueForKey:@"albumName"]);
        NSLog(@"Album age: %@", [info valueForKey:@"albumReleaseDate"]);
        NSLog(@"Album gender: %@", [info valueForKey:@"albumGenre"]);
        
    }
    [fetchRequest release];
    
    [self dismissModalViewControllerAnimated:YES];
}

8.) Go ahead and run the app and test it out to make sure everything is working up to this point.

9.) Now let’s add a third tab that will allow us to search for an artist and display all of their albums. Add a Tab Bar Item to the Tab bar and the view should look like this.

Search View

Where it has a UISearchBar and a UITableView.

10.) We’ll have to pass the managed object context to this view just as we did with the first and second view controllers. Go to the app delegate header file and import the new view controller. Then create an ivar for it.

#import 
#import "FirstViewController.h"
#import "SecondViewController.h"
#import "SearchViewController.h"

@interface CoreDataTutorial5AppDelegate : NSObject  
{
    
@private
    NSManagedObjectContext *managedObjectContext;
    NSManagedObjectModel *managedObjectModel;
    NSPersistentStoreCoordinator *persistentStoreCoordinator;
    
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UITabBarController *tabBarController;

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

@property (nonatomic, retain) IBOutlet FirstViewController *firstViewController;
@property (nonatomic, retain) IBOutlet SecondViewController *secondViewController;
@property (nonatomic, retain) IBOutlet SearchViewController *searchViewController;

- (NSURL *)applicationDocumentsDirectory;
- (void)saveContext;

@end

11.) Synthesize our new ivar in the implementation file. and pass the managed object context in the didFinishLaunchingWithOptions method.

#import "CoreDataTutorial5AppDelegate.h"

@implementation CoreDataTutorial5AppDelegate

@synthesize window=_window;
@synthesize tabBarController=_tabBarController;
@synthesize firstViewController, secondViewController, searchViewController;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.firstViewController.managedObjectContext = self.managedObjectContext;
    self.secondViewController.managedObjectContext = self.managedObjectContext;
    self.searchViewController.managedObjectContext = self.managedObjectContext;
    self.window.rootViewController = self.tabBarController;
    [self.window makeKeyAndVisible];
    return YES;
}

12.) Open MainWindow.xib and connect the searchViewController outlet to the SearchViewController in the Tab Bar Controller.

13.) Open SearchViewController.h and declare two NSArray ivars. One to hold our fetchedObjects (the search results), and one to hold the list of albums from the artiste returned by our search. Declare an ivar for Artist and import Artist into the header file. Create IBOutlets fort our table view and search bar and finally create ivars for the fetchedResultsController and the managedObjectContext.

#import 
#import "Artist.h"

@interface SearchViewController : UIViewController 
{
    NSArray *fetchedObjects;
    NSArray *albumArray;
    Artist *artist;
}

@property (nonatomic, retain) IBOutlet UITableView *searchResultsTableView;
@property (nonatomic, retain) IBOutlet UISearchBar *mySearchBar;
@property (nonatomic, retain) NSFetchedResultsController    *fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext        *managedObjectContext;

@end

14.) Now open up SearchViewController.m and import both Artist and Album and synthesize all our ivars.

#import "SearchViewController.h"
#import "Artist.h"
#import "Album.h"

@implementation SearchViewController

@synthesize searchResultsTableView, mySearchBar, fetchedResultsController, managedObjectContext;

Then implement viewWillAppear. We’ll use this to clear our search bar.

- (void)viewWillAppear:(BOOL)animated
{
    mySearchBar.text = @""; 
}

15.) Now let’s implement the viewDidLoad method. We’ll set up our core data elements here.

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //    NSFetchRequest needed by the fetchedResultsController
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    
    //    NSSortDescriptor tells defines how to sort the fetched results
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"artistName" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];
    
    //    fetchRequest needs to know what entity to fetch
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Artist" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
    [sortDescriptors release];
    [sortDescriptor release];
    
    fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Root"];
    
    [fetchRequest release];
}

16.) Next implement the searchBarButtonClicked method. This is where the search takes place and we’ll set the albumArray here.

- (void) searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
{
    NSLog(@"searchBarSearchButtonClicked");
    
    NSError *error = nil;
    
    // We use an NSPredicate combined with the fetchedResultsController to perform the search
    if (self.mySearchBar.text !=nil)
    {
        NSPredicate *predicate =[NSPredicate predicateWithFormat:@"artistName  contains[cd] %@", self.mySearchBar.text];
        [fetchedResultsController.fetchRequest setPredicate:predicate];
    }
    
    if (![[self fetchedResultsController] performFetch:&error])
    {
        // Handle error
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        exit(-1);  // Fail
    }
    
    // this array is just used to tell the table view how many rows to show
    fetchedObjects = fetchedResultsController.fetchedObjects;
    
    // Handle the case where search returns nothing
    if ([fetchedObjects count] > 0) 
    {
        artist = [fetchedObjects objectAtIndex:0];
        
        NSSet *artistSet = artist.album;
        albumArray = [artistSet allObjects];
    }
    else
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Search Results" message:@"Your search produced no results, please try again." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
        [alert show];
        [alert release];

    }
    
    // dismiss the search keyboard
    [mySearchBar resignFirstResponder];
    
    // reload the table view
    [searchResultsTableView reloadData];
}

17.) Use the albumArray to set the numberRowsInSection.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [albumArray count];
}

18.) And finally implement the cellForRowAtIndexPath method like this to display the artist and their albums.

- (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] autorelease];
    }
    
    // Configure the cell...
    cell.textLabel.text = artist.artistName;
    
    NSSet *artistSet = artist.album;
    NSArray *objectsArray = [artistSet allObjects];

    for (int i = 0; i < [objectsArray count]; i++) 
    {
        Album *album = [objectsArray objectAtIndex:indexPath.row]; 
        cell.detailTextLabel.text = album.albumName;

    }
    
    return cell;
}

Here's what it looks like when done.

Finished Screen Shot

And here's the code.



This entry was posted in Data and tagged , . Bookmark the permalink.

21 Responses to iPhone App Development Tutorial – Core Data part 2 – One to Many Relationship

  1. 7 says:

    This helped a lot thanks!

  2. Paul says:

    Excellent tutorial.

    However, where in the code do we know the artist for whom we are saving the album for? This would be the one-to-many relationship… Thanks. -Paul

    • Kent says:

      Step 7, line number 9 in the code snippet. We are setting album.artist, yet if you look when we created the Album entity we did not create an artist attribute. The artist comes from the relationship.

  3. John says:

    Pretty sloppy tutorial actually.

    Your screenshots setting up the one-to-many in Xcode are wrong. e.g. you have “to-many” ticked for both entities.

    The relationship in the Artist object which points to the “many” entity should have been named “albums” not “album”, to make it clear that it is a “to-many” relationship.

    And in Step 18:


    NSSet *artistSet = artist.album;

    would be much clearer as:


    NSSet *albums = artist.albums;

    • Kent says:

      You are correct the screenshot in step #6 was wrong, I’ve fixed it. The code itself was correct, the screenshot was just a mistake.

      And the code would be easier to read with your naming suggestions, but that doesn’t keep it from running or keep someone from seeing how to set this up.

    • --Nick/ says:

      @John said: “Pretty sloppy tutorial actually.”

      I couldn’t disagree more, actually.

      Your points are well-taken, but it’s not just what you say, it’s how you choose to say it. If your first sentence were removed, you’d have a useful post *without* an unnecessary insult.

  4. Bagusflyer says:

    Good article. But I have one question. How to delete a album? Thanks

  5. Alximik says:

    Super! Thank you very much!

  6. Nuy says:

    The Project of your Tutorial doesn’t work for me :-) it doesn’t save the albums and the enterAlbumView won’t reset if there is no content

  7. gary says:

    Great! Thanks.
    But now ,i got a question :
    In “SearchViewController.m” the tableview will display the artist’ set (when you get search result), and i want to display the result in order with the Album’s attribute “Release Date”.So , How can do that?

  8. Suparjito Teo says:

    Thank you so much! your article is great! :D

  9. Alexander Sharma says:

    How can I prepopulate the application with album artist data.

    please email me at alexander.sharma@gmail.com

    thanks

  10. Alexander Sharma says:

    Hi

    How can I prepopulate Album and Artist with Data.

    THanks!!!

  11. Al Dockett says:

    Hi this article has helped me a lot.
    I am puzzled by this piece of code -
    self.firstViewController.managedObjectContext = self.managedObjectContext;
    self.secondViewController.managedObjectContext = self.managedObjectContext;
    self.window.rootViewController = self.tabBarController;

    Why does this work? I always alloc and init view controllers first – then I set their properties. Is this pattern ok for any view controller property type or is it because it a managedObjectContext?

    Thanks,

    -Al

  12. ctlockey says:

    Where does the searchBarSearchButtonClicked: method get called? When I run the final form of the app and attempt to search, nothing happens and the keyboard does not dismiss.

    • Kent says:

      Have you tried downloading the code and running it? It works for me, but you must put in album information.

      Then you can compare that to your code and see what’s different.

      Good Luck

  13. prem says:

    this really help for me….
    great tutorial

Add Comment Register



Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>