iamluisg
iamluisg

Reputation: 102

Core data one-to-many relationship and Table view

Well, I have been beating my head over this one for some time now, definitely over a week and I just don't know where I am going wrong. Your help would be greatly appreciated!!

Here is the app idea. I have two entities modeled in core data. The Tycoon entity has a one to many relationship with Speech, so each Tycoon is capable of having more than one speech. In the Tycoon entity, the relationship is titled 'TycoonToSpeech' and in the Speech entity, the relationship to Tycoon is titled 'SpeechToTycoon' (this is not a one to many relationship).

Problem 1: I thought I had created my relationships in the appdelegate.m file correctly, but I am not sure. Here is the code in my appdelegate to generate content. Should I do anything more with the NSMutableSets???

NSManagedObjectContext *context = [self managedObjectContext];


NSEntityDescription *tycoonEntity = [NSEntityDescription entityForName:@"Tycoon" inManagedObjectContext:context];

NSManagedObject *steve = [NSEntityDescription insertNewObjectForEntityForName:[tycoonEntity name] inManagedObjectContext:context];

[steve setValue:@"Steve Jobs" forKey:@"Name"];
int orgId = [steve hash];
[steve setValue:[NSNumber numberWithInt:orgId] forKey:@"Id"];

NSManagedObject *warren = [NSEntityDescription insertNewObjectForEntityForName:[tycoonEntity name] inManagedObjectContext:context];
[warren setValue:@"Warren Buffet" forKey:@"Name"];
int orgId2 = [warren hash];
[warren setValue:[NSNumber numberWithInt:orgId2] forKey:@"Id"];


NSEntityDescription *speechEntity = [NSEntityDescription entityForName:@"Speech" inManagedObjectContext:context];
NSManagedObject *stanford = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[stanford setValue:[NSNumber numberWithInt:[stanford hash]] forKey:@"speechId"];
[stanford setValue:@"Stanford" forKey:@"speechName"];

NSManagedObject *wwdc = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[wwdc setValue:[NSNumber numberWithInt:[wwdc hash]] forKey:@"speechId"];
[wwdc setValue:@"WWDC" forKey:@"speechName"];

NSManagedObject *shareHolder = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[shareHolder setValue:[NSNumber numberWithInt:[shareHolder hash]] forKey:@"speechId"];
[shareHolder setValue:@"Shareholder's Meeting" forKey:@"speechName"];

NSManagedObject *columbia = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[columbia setValue:[NSNumber numberWithInt:[columbia hash]] forKey:@"speechId"];
[columbia setValue:@"Columbia" forKey:@"speechName"];


NSMutableSet *jobsSpeeches = [steve mutableSetValueForKey:@"TycoonToSpeech"];
[jobsSpeeches addObject:stanford];
[jobsSpeeches addObject:wwdc];


NSMutableSet *warrenSpeeches = [warren mutableSetValueForKey:@"TycoonToSpeech"];
[warrenSpeeches addObject:shareHolder];
[warrenSpeeches addObject:columbia];

Problem 2: The main and really annoying problem, I don't know how to make didSelectRowAtIndexPath in my root view controller work so that when I click on 'Steve Jobs' in the table view, it brings up only his speeches and when I click on Warren Buffet it only brings up his speeches. I have banged my head against the wall trying to get this right. At the moment, when I click on either Steve Jobs or Warren Buffet it takes me to a new table view showing 'Steve Jobs' and 'Warren Buffet', so basically the exact same table view as the original!!! Here is my code for didSelectRowAtIndexPath.

    // Create a new table view of this very same class.
    RootViewController *rootViewController = [[RootViewController alloc]
                                              initWithStyle:UITableViewStylePlain];

    // Pass the managed object context
    rootViewController.context = self.context;
    NSPredicate *predicate = nil;
    // Get the object the user selected from the array
    NSManagedObject *selectedObject = [entityArray objectAtIndex:indexPath.row];


    rootViewController.entityName = @"Speeches";

    // Create a query predicate to find all child objects of the selected object. 
    predicate = [NSPredicate predicateWithFormat:@"SpeechToTycoon == %@", selectedObject];


    [rootViewController setEntitySearchPredicate:predicate];

    //Push the new table view on the stack
    [self.navigationController pushViewController:rootViewController animated:YES];
    [rootViewController release];

Newly Edited here**

OK, here is the main code in my SpeechViewController.m file. The numberOfRowsInSection method seems to be working, but the real problem is the cellForRowAtIndexPath that is the problem. I know I am probably doing something very stupid, but I don't know what it is.

- (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];
}
NSArray *speeches = (NSArray *)tycoon.TycoonToSpeech;
// Configure the cell...
cell.textLabel.text = [[speeches objectAtIndex:indexPath.row] retain];
return cell;

}

New speech view controller to language points view controller

I added this in the app delegate to add objects to my language points entity.

NSEntityDescription *langPointEntity = [NSEntityDescription entityForName:@"LanguagePoints" inManagedObjectContext:context];

NSManagedObject *pointOne = [NSEntityDescription insertNewObjectForEntityForName:[langPointEntity name] inManagedObjectContext:context];
[pointOne setValue:[NSNumber numberWithInt:[pointOne hash]] forKey:@"LangId"];
[pointOne setValue:@"Drop out" forKey:@"LangPoint"];
[pointOne setValue:stanford forKey:@"LanguagePointsToSpeech"];

Then in my speechViewController.m file I created an NSArray called speechInfo, then here is my didSelectRowAtIndexPath method.

LangPointsViewController *langPointsView = [[LangPointsViewController alloc] initWithNibName:@"LangPointsViewController" bundle:[NSBundle mainBundle]];
Speech *speech = [speechInfo objectAtIndex:indexPath.row];
langPointsView.speech = speech;
[self.navigationController pushViewController:langPointsView animated:YES];
[langPointsView release];

I think this is where things are going wrong, I don't think I am getting the object here correctly because I do an NSLog on speech.Name in the LangPointsViewController and it comes up null, where as if I do an NSLog for tycoon.Name in the SpeechViewController it prints the person's name I clicked on. Is it the NSArray that's incorrect.

Upvotes: 1

Views: 3217

Answers (1)

Marcus S. Zarra
Marcus S. Zarra

Reputation: 46718

Problem #1

First your relationship names are bad. They should start with a lower case and they should simply describe what is on the other side of the relationship. You already know where you are. So name them tycoon and speeches respectively. Second, because relationships are bi-directional you can construct your data easier, see the following:

NSManagedObjectContext *context = [self managedObjectContext];

NSManagedObject *steve = [NSEntityDescription insertNewObjectForEntityForName:@"Tycoon" inManagedObjectContext:context];

[steve setValue:@"Steve Jobs" forKey:@"Name"];
int orgId = [steve hash];
[steve setValue:[NSNumber numberWithInt:orgId] forKey:@"Id"];

NSManagedObject *warren = [NSEntityDescription insertNewObjectForEntityForName:[tycoonEntity name] inManagedObjectContext:context];
[warren setValue:@"Warren Buffet" forKey:@"Name"];
int orgId2 = [warren hash];
[warren setValue:[NSNumber numberWithInt:orgId2] forKey:@"Id"];

NSManagedObject *stanford = [NSEntityDescription insertNewObjectForEntityForName:@"Speech" inManagedObjectContext:context];
[stanford setValue:[NSNumber numberWithInt:[stanford hash]] forKey:@"speechId"];
[stanford setValue:@"Stanford" forKey:@"speechName"];
[stanford setValue:warren forKey:@"tycoon"];

NSManagedObject *wwdc = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[wwdc setValue:[NSNumber numberWithInt:[wwdc hash]] forKey:@"speechId"];
[wwdc setValue:@"WWDC" forKey:@"speechName"];
[wwdc setValue:warren forKey:@"tycoon"];

NSManagedObject *shareHolder = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[shareHolder setValue:[NSNumber numberWithInt:[shareHolder hash]] forKey:@"speechId"];
[shareHolder setValue:@"Shareholder's Meeting" forKey:@"speechName"];
[shareHolder setValue:warren forKey:@"tycoon"];

NSManagedObject *columbia = [NSEntityDescription insertNewObjectForEntityForName:[speechEntity name] inManagedObjectContext:context];
[columbia setValue:[NSNumber numberWithInt:[columbia hash]] forKey:@"speechId"];
[columbia setValue:@"Columbia" forKey:@"speechName"];
[columbia setValue:warren forKey:@"tycoon"];

Note that I am setting the relationship from the Speech side. Core Data will handle the other side.

Problem #2

Without looking at your entire root view controller it is hard to say what you are doing "wrong". However, I would not re-use a view controller to display two different types of objects. That is bad form and leads to code cruft.

Instead I would create a second "master" view controller that is designed to handle speeches. Then you can just pass in the tycoon and your SpeechViewController can run just off an array derived from the relationship or it can build up its fetched results controller internally. Even of greater value is the ability to customize its cells, title, etc. to fit the data it is displaying.

Problem #3

First a comment about the new code you added:

SpeechViewController *speechView = [[SpeechViewController alloc] initWithNibName:@"SpeechViewController" bundle:[NSBundle mainBundle]];
Tycoon *tycoons = (Tycoon *)[fetchedResultsController objectAtIndexPath:indexPath];
speechView.tycoons = tycoons;
[self.navigationController pushViewController:speechView animated:YES];

There is ZERO reason to cast out of a -[NSFetchedResultsController objectAtIndexPath:]. Any method that returns id can be assigned to any pointer.

Casting is a lie to the compiler and should be avoided at all cost.

Now, leaving that aside, your code is fine. I would name the property 'tycoon' instead of 'tycoons' because you are passing in a single Tycoon entity.

On the SpeechViewController side you can now access the speeches associated with that Tycoon entity and display them however you choose.

What is the issue you are seeing?

Problem #4

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
    NSSet *speechesSet = [[self tycoon] valueForKey:@"TycoonToSpeech"];
    // Sort the speeches
    NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSArray *sortedSpeeches = [speechesSet sortedArrayUsingDescriptors:[NSArray arrayWithObject:nameSort]];
    // Configure the cell...
    id speech = [sortedSpeeches objectAtIndex:[indexPath row]];
    [[cell textLabel] setText:[speech valueForKey:@"name"]];

    return cell;
}

Ok starting at the top. First, a relationship will return a NSSet not a NSArray. The results from a relationship are unordered. Therefore we grab the results as a NSSet and then sort them. I guessed that there is a name attribute on your Speech entity.

Once we have them sorted we can grab the item at the row. This will return a Speech entity. I assigned it to id because we are going to be using KVC anyway.

Finally, I use KVC to get the name attribute from the Speech entity and assign it to the textLabel of the cell.

As for your -tableView: numberOfRowsInSection:, that one is far simpler:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[[self tycoon] valueForKey:@"TycoonToSpeech"] count];
}

Because you only have one section you only need to return the count of speeches.

I mentioned before that your attribute names need some work. Since Tycoon is just an object and TycoonToSpeech is just a property on that object, the property should really be called speeches because it is no more special than any other property on an object. The attribute name flows better in your code and makes your code easier to consume. In addition, Objective-C naming conventions call for attributes aka properties to start with a lower case.

Upvotes: 9

Related Questions