NSFetchedResultsController: Multiple FRCs, Delegate Error when Updating
Asked Answered
R

2

0

Objective: Using FRC, sort Section's by startDate, an NSDate attribute, but want Today's date Section to appear before Upcoming dates Section.

I followed Apple's code using a transient property sectionIdentifier. Apple's sample code. and started with this project first: OneFRC

I soon realized that this may not be possible with just one FRC (I could be wrong).

Next, I decided to take a stab at this with 3 FRCs: ThreeFRC.

TableView sections now appears in the Order that I want:

Section 0: Today

Section 1: Upcoming

Section 2: Past

However, adding data triggers FRC delegates, and I get the following error:

CoreData: error: Serious application error.  An exception was caught from the 
delegate of NSFetchedResultsController during a call to 
-controllerDidChangeContent:.  Invalid update: invalid number of rows in section 0.
The number of rows contained in an existing section after the update (4) must be 
equal to the number of rows contained in that section before the update (3), plus 
or minus the number of rows inserted or deleted from that section (0 inserted, 
0 deleted) and plus or minus the number of rows moved into or out of that section 
(0 moved in, 0 moved out). with userInfo (null)

Again, I would love to be able to accomplish my objective with 1 FRC, but I can't seem to figure out how.

I have been trying to resolve this for 4 days now! If this issue doesn't get resolved on SO, I think I may reach out to Apple for Developer support. And in the event that I do, I'll post the resolution here so others can benefit.

Projects are available on Github:

One FRC

Three FRC

EDIT

Thanks to @blazejmar, I was able get rid of the rows error. However, now I get an error when I attempt to add sections.

2014-11-03 16:39:46.852 FRC[64305:60b] CoreData: error: Serious application error.  
An exception was caught from the delegate of NSFetchedResultsController during a 
call to -controllerDidChangeContent:.  Invalid update: invalid number of sections.  
The number of sections contained in the table view after the update (2) must be 
equal to the number of sections contained in the table view before the update (1), 
plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). 
with userInfo (null)

Steps to reproduce the error in Three FRC:

1. Launch App -> 
2. Tap Generate Data -> 
3. Tap View in FRC -> 
4. Tap back to the RootVC ->
5. Change the system date to a month from Today -> 
6. Tap View in FRC and only one section `Past` should appear. -> 
7. Tap `Add Data`.  
8. The error should appear in the log. 
Rentfree answered 3/7, 2014 at 14:13 Comment(0)
S
1

In your ThreeFRC project there are some issues:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
    self.numberOfSectionsInTV = 0;
    [self fetchData];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView reloadData];
    [self.tableView endUpdates];
}

You shouldn't use fetchData inside FRC delegate. Methods are called in proper order (before, during and after update) so inside callbacks you have consistent state of context. Also it's not the best idea to use reloadData before endUpdates(it's applying all changes you provided earlier) and reloadData is erasing everything and building it from scratch. This is most likely causing the crash.

Other thing I've spotted that may be buggy is handling of updates. If you have 3 separate FRC without sections you won't get section update callback in FRC delegate. But if some objects appear in one of the FRC's then you should detect that and manually insert them.

Using just reloadData in controllerDidChangeContent would be enough, but this isn't the best solution, as you won't get any animations. The proper way would be to handle all the cases: deleting all objects from one of FRCs (and then deleting section manually from TableView), inserting first object into FRC (then you should create new section at proper indexPath).

Swoon answered 3/7, 2014 at 22:57 Comment(2)
So, this took away the rows error; however, you are right, I'm getting an insert error. Please see the edit in the question. Can you please elaborate on how to delete objects from FRC's and manually insert them? Is it just setting it to nil and re-initialize the FRC? I have 3 separate FRCs, one for each section. I tried to change the section number in controllerWillChangeContent:controller but that didn't work. Please see my code change here: github.com/KausiAhmed/ThreeFRC/blob/master/FRC/FRCTableVC.m Thank you!Rentfree
I can now insert objects if sections are present. I can't figure out how to add or delete sections.Rentfree
T
0

i looked at your ThreeFRC project and noticed that in - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView you check which FRCs contain objects and that would determine the number of sections. this makes logical sense, but really confuses the FRC delegate when adding/deleting "sections" (or, when your other FRCs suddenly have objects). For example, you only have a Past section (1 section), but then the data changes such that you now also have a Today section. Since sectionPastFRC or the other FRCs didn't have any section changes, there are no calls to - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type, and though you should have 2 sections now, there were no calls to add, delete, or move sections. you'd have to update the sections manually somehow, which may be a pain.

here's the workaround i suggest: since you will ALWAYS have at most one section for each FRC, you should just return 3 in - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView. This is so there will no longer be any problem in adding/deleting a section because they were all already there. Anyway, if, for example, the Today section has no objects, just return 0 in - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section. Just make sure that in - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section, if fetchedObjects==0, return nil, so that it also won't display the section header if that section has no objects. And in your FRC delegate didChangeObject, just always adjust the indexPath and newIndexPath before performing changes on the tableView.

note that this workaround will only work if you already know the maximum number of sections that the FRCs (except the last FRC) will need. it is NOT a solution for all implementations of multiple FRCs in a single table view. i've actually used this solution in a project where i had 2 FRCs for one tableView, but the first FRC would only always take up 1 section, while the second FRC could have any number of sections. i always just had to adjust the sections +1 for changes in the second FRC.

i've actually tried applying the changes i mentioned above into your code, and haven't been getting errors. here are the parts i changed in the UITableViewDataSource:

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

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSInteger rows = 0;
    switch (section) {
        case 0:
        {
            rows = [[self.sectionTodayFRC fetchedObjects]count];
            break;
        }
        case 1:
        {
            rows = [[self.sectionUpcomingFRC fetchedObjects]count];
            break;
        }
        case 2:
        {
            rows = [[self.sectionPastFRC fetchedObjects]count];
            break;
        }
    }
    NSLog(@"Section Number: %i Number Of Rows: %i", section,rows);
    return rows;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    NSString *header;
    switch (section) {
        case 0:
        {
            if ([[self.sectionTodayFRC fetchedObjects]count] >0)
            {
                header = @"Today";
            }
            break;
        }
        case 1:
        {
            if ([[self.sectionUpcomingFRC fetchedObjects]count] >0)
            {
                header = @"Upcoming";
            }
            break;
        }
        case 2:
        {
            if ([[self.sectionPastFRC fetchedObjects]count] >0)
            {
                header = @"Past";
            }
            break;
        }

    }

    return  header;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                       reuseIdentifier:CellIdentifier];
    }
    Meeting *meeting;
    switch (indexPath.section) {
        case 0: 
            if ([[self.sectionTodayFRC fetchedObjects]count] > 0)
            {
                meeting = [[self.sectionTodayFRC fetchedObjects] objectAtIndex:indexPath.row];
            }
            break;

        case 1:
            if ([[self.sectionUpcomingFRC fetchedObjects]count] > 0)
            {
                meeting = [[self.sectionUpcomingFRC fetchedObjects] objectAtIndex:indexPath.row];
            }
            break;

        case 2:
            if ([[self.sectionPastFRC fetchedObjects]count] > 0)
            {
                meeting = [[self.sectionPastFRC fetchedObjects] objectAtIndex:indexPath.row];
            }
            break;
    }

    cell.textLabel.text = meeting.title;
    return cell;
}

and for the NSFetchedResultsControllerDelegate:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    NSLog(@"Inside didChangeObject:");

    NSIndexPath *modifiedIndexPath;
    NSIndexPath *modifiedNewIndexPath;

    if (controller == self.sectionTodayFRC)
    {
        modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:0];
        modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:0];
    }
    else if (controller == self.sectionUpcomingFRC)
    {
        modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:1];
        modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:1];
    }
    else if (controller == self.sectionPastFRC)
    {
        modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:2];
        modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:2];
    }

    switch(type) {

        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:@[modifiedNewIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeDelete:
            NSLog(@"frcChangeDelete");
            [self.tableView deleteRowsAtIndexPaths:@[modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeUpdate:
            NSLog(@"frcChangeUpdate");
            [self.tableView reloadRowsAtIndexPaths:@[modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeMove:
            NSLog(@"frcChangeDelete");
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:modifiedNewIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }

}

i hope this helps someone!

Teacher answered 10/9, 2015 at 4:42 Comment(1)
@ Mariel Dee, I have a question similar to this one that I was hoping you might give me some insight on. #41310793Buss

© 2022 - 2024 — McMap. All rights reserved.