2011年12月30日星期五

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

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.

1 条评论:

  1. Valuable information and excellent design you got here! I would like to thank you for sharing your thoughts and time into the stuff you post!! Thumbs up
    App Development Company

    回复删除