amitsbajaj
amitsbajaj

Reputation: 1304

NSFetchedResultsController not loading all entires

I have a simple application with a UITabBarController and 2 UITableViewControllers in each UITabBar. The first UITableViewController is called Videos and the second is called Languages.

Videos will represent just the languages that have videos and Languages will display all of the languages.

For test purposes, I am populating the Core Data Entities from the AppDelegate.

My Core Data model is below:

enter image description here

The method that's populating the Core Data looks like this:

- (void)loadVideosTab
{
    // Here is where we'll load up the Videos tab.
    NSManagedObjectContext *context = [self managedObjectContext];
    NSString *chinese = @"Chinese";
    NSString *dutch = @"Dutch";
    NSString *english = @"English";
    NSString *french = @"French";
    NSString *italian = @"Italian";
    NSString *punjabi = @"Punjabi";
    NSString *spanish = @"Spanish";
    NSString *tamil = @"Tamil - தமிழ்";

    Language *language = [NSEntityDescription insertNewObjectForEntityForName:@"Language" inManagedObjectContext:context];


    Video *chineseLanguage = (Video *)[Video videoLanguage:chinese inManagedObjectContext:context];
    language.videos = chineseLanguage;
    Video *dutchLanguage = (Video *)[Video videoLanguage:dutch inManagedObjectContext:context];
    language.videos = dutchLanguage;
    Video *englishLanguage = (Video *)[Video videoLanguage:english inManagedObjectContext:context];
    language.videos = englishLanguage;
    Video *frenchLanguage = (Video *)[Video videoLanguage:french inManagedObjectContext:context];
    language.videos = frenchLanguage;
    Video *italianLanguage = (Video *)[Video videoLanguage:italian inManagedObjectContext:context];
    language.videos = italianLanguage;
    Video *punjabiLanguage = (Video *)[Video videoLanguage:punjabi inManagedObjectContext:context];
    language.videos = punjabiLanguage;
    Video *spanishLanguage = (Video *)[Video videoLanguage:spanish inManagedObjectContext:context];
    language.videos = spanishLanguage;
    Video *tamilLanguage = (Video *)[Video videoLanguage:tamil inManagedObjectContext:context];
    language.videos = tamilLanguage;

    NSError *error = nil;
    if (![context save:&error])
    {
        // Error
    }    
}

The category I'm calling on the Video Entity is:

+ (Video *)videoLanguage:(NSString *)name inManagedObjectContext:(NSManagedObjectContext *)context
{
    Video *video = nil;

    // Creating a fetch request to check whether the video already exists, calling from the AppDelegate
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Video"];
    request.predicate = [NSPredicate predicateWithFormat:@"language = %@", name];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"language" ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

    NSError *error = nil;
    NSArray *videoTitles = [context executeFetchRequest:request error:&error];
    if (!videoTitles)
    {
        // Handle Error
    }
    else if (![videoTitles count])
    {
        // If the video count is 0 then create it
        video = [NSEntityDescription insertNewObjectForEntityForName:@"Video" inManagedObjectContext:context];
        video.language = name;

    }
    else
    {
        // If the object exists, just return it
        video = [videoTitles lastObject];
    }
    return video;

}

So in the Video tab, I am using NSFetchedResultsController as so:

- (NSFetchedResultsController *)fetchedResultsController
{
    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
    if (_fetchedResultsController != nil)
    {
        return _fetchedResultsController;
    }
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];


    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Video" inManagedObjectContext:managedObjectContext];
    fetchRequest.entity = entity;
    NSPredicate *d = [NSPredicate predicateWithFormat:@"language != nil"];
    [fetchRequest setPredicate:d];
    NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"language" ascending:YES];


    fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
    fetchRequest.fetchBatchSize = 20;
    NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    self.fetchedResultsController = theFetchedResultsController;
    _fetchedResultsController.delegate = self;
    return _fetchedResultsController;
}

And the cellForRow is:

VideoTabTableViewCell *customCell = (VideoTabTableViewCell *)cell;

Video *video = [self.fetchedResultsController objectAtIndexPath:indexPath];

customCell.videoTabCellLabel.text = video.language;
customCell.videoTabCellLabel.textColor = [UIColor whiteColor];

That successfully pulls in the languages.

Now, for test purposes, I want the Languages tab to do the exact same thing. The reason I have a Language Entity is because the Languages tab will be a combination of languages that have videos and leaflets.

So in the Languages tab, my fetchedResultsController is:

- (NSFetchedResultsController *)fetchedResultsController
{
    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
    if (_fetchedResultsController != nil)
    {
        return _fetchedResultsController;
    }
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];


    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Language" inManagedObjectContext:managedObjectContext];
    fetchRequest.entity = entity;
//    NSPredicate *d = [NSPredicate predicateWithFormat:@"name != nil"];
//    [fetchRequest setPredicate:d];
    NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"videos.language" ascending:YES];


    fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
    fetchRequest.fetchBatchSize = 20;
    NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    self.fetchedResultsController = theFetchedResultsController;
    _fetchedResultsController.delegate = self;
    return _fetchedResultsController;
}

And the cellForRow is:

VideoTabTableViewCell *customCell = (VideoTabTableViewCell *)cell;

Language *languages = [self.fetchedResultsController objectAtIndexPath:indexPath];

customCell.videoTabCellLabel.text = languages.videos.language;
customCell.videoTabCellLabel.textColor = [UIColor whiteColor];

Update - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"langauges tab";

    VideoTabTableViewCell *cell = (VideoTabTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    [self configureCell:cell atIndexPath:indexPath];
    return cell;

}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id  sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.languagesTabTableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    // The boiler plate code for the NSFetchedResultsControllerDelegate

    UITableView *tableView = self.languagesTabTableView;

    switch(type) {

        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];

            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        default:
            break;

    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    [self.languagesTabTableView endUpdates];
}

I am calling the fetchedResultsController from viewDidLoad and reloading the table from viewWillAppear.

However, the situation on the Languages tab is that it only shows the last language that I added in, and every time I launch, I creates another new empty cell.

enter image description here

I just can't figure out what's actually going on with this.

The clarify, the main issue here isn't the phantom cells, but why the Language is only picking up the last entry and not the others.

Any guidance would really be appreciated.

Upvotes: 1

Views: 114

Answers (2)

pbasdf
pbasdf

Reputation: 21536

I think you need to refine your data model:

First, the relationship from Language to Video has a plural name, videos, which suggests you want each Language object to be related to many Video objects. But the image from the model editor shows that the relationship is currently defined as "to-one".

Second, why have a Language entity, and a language attribute on the Video entity? From your code, it looks like the language attribute is a string representing the name of the language. I would suggest you should instead have a name attribute on the Language entity.

In addition to those points, the crux of your problem is your loadVideosTab method. It creates only one Language object:

Language *language = [NSEntityDescription insertNewObjectForEntityForName:@"Language" inManagedObjectContext:context];

and then creates (or fetches) several Video objects, and establishes the relationship with the (one) Language, e.g.:

Video *chineseLanguage = (Video *)[Video videoLanguage:chinese inManagedObjectContext:context];
language.videos = chineseLanguage;

However, because the videos relationship is "to-one", the Language object can be related to only one Video. So each time you set the relationship, the old one is removed. At the end of your method, your one Language is related only to the Tamil Video object. The other seven Video objects are not related to any Language.

Each run creates a new Language, and establishes a relationship to the Tamil Video. Because each Video can have only one Language, when that relationship is made, CoreData drops the Tamil video's relationship to the "old" Language object. Hence you end up with several "old" Language objects that are not related to any Video - these are the blank lines in your Language table view.

EDIT

A note on handling to-many relationships: the relationship is represented as an NSSet. If you are setting a relationship (and want to remove existing relationships), you can use:

language.videos = [NSSet setWithObject:tamilLanguage];

Or to set multiple objects all at once, use:

language.videos = [NSSet setWithArray:@[chineseLanguage, dutchLanguage, tamilLanguage]];

These blow out existing relationships, so to add a relationship, whilst keeping the existing, use NSMutableSet:

NSMutableSet *videos = [language mutableSetValueForKey:@"videos"];
[videos addObject:tamilLanguage];

(and similarly for removeObject to remove just one of the related objects). I find these methods rather unintuitive, so if I have a relationship whose inverse is "to-one", I use that instead:

tamilLanguage.videoLanguage = language;

Having said that, I'm now wondering whether that inverse relationship should also be "to-many" - can a Video have many Languages?

Upvotes: 1

Roy Falk
Roy Falk

Reputation: 1741

I'm not familiar with managed objects but this looks like it behaves like a dictionary. Multiple insertNewObjectForEntityForName with the same name would mean that the last one overwrites the rest.

The Apple doc seems to indicate this too:

Discussion
This method makes it easy for you to create instances of a given entity without worrying about the details of managed object creation. The method is conceptually similar to the following code example.

NSManagedObjectModel *managedObjectModel =
        [[context persistentStoreCoordinator] managedObjectModel];
NSEntityDescription *entity =
        [[managedObjectModel entitiesByName] objectForKey:entityName];
NSManagedObject *newObject = [[NSManagedObject alloc]
            initWithEntity:entity insertIntoManagedObjectContext:context];
return newObject;

Note the above objectForKey! Same as NSDictionary

=================================================================

dequeueReusableCellWithIdentifier doesn't always return a non-nil object. It's used to reuse existing cells. But if there isn't one available, you need to alloc/init one.

Kudos to Jay Versluis

Something like:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath   {
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
  if (cell == nil) {
     cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
  }

  // populate your cell
  return cell;

}

Upvotes: 1

Related Questions