0

I am working upon a Core-data app which involves collapsable sections.

Basically, if I move a managed object across section, or just delete it when its section is opened, I get the following error:

2014-04-28 19:38:44.690 uRSS[663: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 rows in section 7. The number of rows contained in an existing section after the update (2) 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)

I believe its about the way I handle changes in the didChange Object method but I am stuck ATM:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch(type) {
        case NSFetchedResultsChangeInsert:
            NSLog(@"MasterViewController::didChangeObject **** Inserting something in %@**** ", _detailViewController.detailItem.category.name);
            if ([_detailViewController.detailItem.category.open boolValue]) {
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            break;

        case NSFetchedResultsChangeDelete:
            NSLog(@"MasterViewController::didChangeObject **** Deleting something in %@**** ", _detailViewController.detailCategory.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
        break;

        case NSFetchedResultsChangeUpdate:
            NSLog(@"MasterViewController::didChangeObject **** Changing something in %@**** ", _detailViewController.detailCategory.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            }
            break;

        case NSFetchedResultsChangeMove:
            NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            if ([_detailViewController.detailItem.category.open boolValue]) {
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            break;
    }
}

My detailView has 2 objects attached to it: detailItem, a feed, and detailCategory, the original Category, should it change.

Category has a name and an openproperty. If open is false, the Category (section) is collapsed.

So far I cannot delete objects or change their category without a crash. If the category is closed, I can delete them.

Whenever creating a new object (which has its own "new" category), it wrongly appears in the currently opened category. If no category is opened, then it will expectedly crete the new category with the new object.

Can somebody tell me how to handle this, please?

UPDATE(1): Here's some more from the UIDataSource:

This is when and how I move a Feed across Categories in my detailView:

-(void) updateCategory:(id)sender
{
    if (!_categoriesPopover) {
        return;
    }
    [_categoriesPopover dismissPopoverAnimated:YES];
    [_mainMOC performBlockAndWait:^{
        Category *category=_categoryView.categoryChosen;
        if (!category) {
            return;
        }

        self.detailItem.category.open=[NSNumber numberWithBool:NO];

        self.detailCategory = self.detailItem.category;
        self.detailItem.category=category;

        self.detailItem.category.open=[NSNumber numberWithBool:YES];

        NSError *error;
        if (![_mainMOC save:&error])
        {
          NSLog(@"DetailViewController::updateCategory Error saving context: %@", error);
        } else {
          [[NSNotificationCenter defaultCenter] postNotificationName:@"feedUpdatedInDetailView" object:nil];
        }
    }];
}

And it happens once I have clicked on the chosen destination Category in this screenshot: My RSS Reader Interface during the Category switch operation. Change Category is the folder item in the bottom menu bar of the detail View

UPDATE(2):

Here's the numberOfRowsInSection method:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
    return [_filteredCategoryArray count];
} else {
    if ([[[self.fetchedResultsController sections][section] objects] count]>0) {
        Category *cat = ((Feed *)[[[self.fetchedResultsController sections][section] objects] objectAtIndex:0]).category;
        return [cat.open boolValue] ? [[self.fetchedResultsController sections][section] numberOfObjects] : 0;
    } else {
        return 0;
    }
}
}

UPDATE(3):

I NSLogged some variables in order to see what was happening and I think we can forget the open/close aspect here.

a) Here's what happens in the DetailView:

-(void) updateCategory:(id)sender
{
if (!_categoriesPopover) {
    return;
}
[_categoriesPopover dismissPopoverAnimated:YES];
Category *category=_categoryView.categoryChosen;
if (!category) {
    return;
}

[_mainMOC performBlockAndWait:^{
    NSLog(@"BEFORE: oldcat: %@ (%d) / newcat: %@ (%d)", _detailItem.category.name, [_detailItem.category.feeds count], category.name, [category.feeds count]);
    self.detailItem.category.open=[NSNumber numberWithBool:NO];

    self.detailCategory = self.detailItem.category;
    self.detailItem.category=category;

    self.detailItem.category.open=[NSNumber numberWithBool:YES];

    NSError *error;
    if (![_mainMOC save:&error])
    {
        NSLog(@"DetailViewController::updateCategory Error saving context: %@", error);
    } else {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"feedUpdatedInDetailView" object:nil];
    }
    NSLog(@"AFTER: oldcat: %@ (%d) / newcat: %@ (%d)", _detailCategory.name, [_detailCategory.feeds count], _detailItem.category.name, [_detailItem.category.feeds count]);
}];
}

b) Here's what's in the MasterView:

            case NSFetchedResultsChangeMove:
            NSLog(@"MVC: oldcat: %@ (%d) / newcat: %@ (%d)", _detailViewController.detailCategory.name, [_detailViewController.detailCategory.feeds count], _detailViewController.detailItem.category.name, [_detailViewController.detailItem.category.feeds count]);

            NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);

            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

c) Here's what the log says:

2014-04-30 18:52:04.771 uRSS[465:60b] BEFORE: oldcat: trucs (2) / newcat: Misc (7)
2014-04-30 18:52:04.771 uRSS[465:60b] MVC: oldcat: trucs (1) / newcat: Misc (8)
2014-04-30 18:52:04.772 uRSS[465:60b] MasterViewController::didChangeObject **** Moving something from trucs to Misc**** 
2014-04-30 18:52:04.772 uRSS[465:60b] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2935.137/UITableView.m:1368
2014-04-30 18:52:04.772 uRSS[465: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 rows in section 5.  The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (1 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)
2014-04-30 18:52:04.773 uRSS[465:60b] [AppDelegate::saveContextChanges] merged.
2014-04-30 18:52:04.774 uRSS[465:8c03] [AppDelegate::saveContextChanges] merged.
2014-04-30 18:52:04.775 uRSS[465:60b] AFTER: oldcat: trucs (1) / newcat: Misc (8)

What I don't get is that the DVC works in a PerformBlockAndWait loop but the MVC gets called with the new values before they seem to be committed on the DVC. How is that possible?

What's wrong???

UPDATE(4): I did as Marcus suggested and removed the MOC save. The logs look better (BEFORE and AFTER happen in the DVC, MVC happens expectedly after in the MVC and the values are correct):

2014-05-01 07:27:39.391 uRSS[742:60b] BEFORE: oldcat: Misc (8) / newcat: trucs (1)
2014-05-01 07:27:39.392 uRSS[742:60b] AFTER: oldcat: Misc (7) / newcat: trucs (2)
2014-05-01 07:27:39.400 uRSS[742:60b] MVC: oldcat: Misc (7) / newcat: trucs (2)

But: I still get the error:

2014-05-01 07:27:39.400 uRSS[742:60b] MasterViewController::didChangeObject **** Moving something from Misc to trucs**** 
2014-05-01 07:27:39.401 uRSS[742:60b] *** Assertion failure in -[UITableView  _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2935.137/UITableView.m:1368
2014-05-01 07:27:39.401 uRSS[742: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 rows in section 5.  The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (8), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 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)

I also see that on restarting the app, the Feed has not been moved which usually happened with a save...

UPDATE(5): I reverted to my original didChangeObject method and it works (moving Feeds across Categories, that is). How can I get a save to "block" the main UI during its processing? performBlockAndWaitobviously doesn't suffice.

mirx
  • 148
  • 11

5 Answers5

2

You have a mismatch between what the delegate methods of NSFetchedResultsController are telling the UITableView and what the methods of UITableViewDataSource are telling the table view.

I would suggest looking at your UITableViewDataSource methods, use the debugger and make sure that they are reporting back the same number of rows as your NSFetchedResultsControllerDelegate methods.

That is the source of this error.

Update

There is a disconnect between your -controller: didChangeObject: atIndexPath: forChangeType: newIndexPath: and one of the methods in UITableViewDataSource. From the error I am guessing it is in -tableView: numberOfRowsInSection: which is the method I wish you would add to your question.

When the NSFetchedResultsControllerDelegate finishes it will then ping the methods implemented from the UITableViewDataSource protocol and ask what the table looks like. Those answers MUST match. If they do not, you get this error.

If you run this in the debugger and put breakpoints in these methods and follow along on a piece of paper you can usually track down where the disconnect is.

Update

Looks like you are storing category open state in Core Data and then responding to the number of rows based on whether the category is open or not. Hopefully I am reading that right.

If that is the case then I would suggest turning that logic off and see if the crash goes away. That will narrow your focus to that logic. It is quite possible that your state is getting out of sync.

Marcus S. Zarra
  • 46,571
  • 9
  • 101
  • 182
  • Hello Marcus, Thanks a lot for your kind answer. I found your book to be the most interesting about CoreData. I am appending a few details regarding your kind comment in the main post, please take a look if you can. It is still not clear what could be wrong in my code (though it's obvious something is). – mirx Apr 29 '14 at 18:02
  • Hey thanks! I added more code from my MasterViewController... Hope it helps. – mirx Apr 29 '14 at 20:24
  • Hello and Thanks again. In my MasterViewController, I took the logic off and the crashes are back. What would be the best way to check the sync between these? I thought they'd all use the same MOC as both ViewControllers are on the same thread? I only use a bgMOC for the mass feed import... – mirx Apr 30 '14 at 05:52
  • 1
    I explained how to walk through this. **Use the debugger**. Put breakpoints in each of the protocol methods. Keep track of how many rows you should have on a piece of paper as you walk through the code. The fact that the crash does not go away indicates that you have a row count error somewhere else. – Marcus S. Zarra Apr 30 '14 at 14:46
  • Hello Marcus, as I am not proficient with the debugger (would you by chance know of a good tutorial/book on the subject?) I NSLogged the variable change state and it seems it has nothing to do with the open Boolean. Could you take a look to UPDATE(3) and give me your advice, please? – mirx Apr 30 '14 at 17:00
1

While not a direct answer to your question, you may find this answer useful as alternative to building your own collapsable table view.: TLIndexPathTools integration with core data for expandable tableView.

As discussed in the link, TLIndexPathTools provides a collapsible table view implementation that integrates with Core Data out of the box.

Community
  • 1
  • 1
Timothy Moose
  • 9,895
  • 3
  • 33
  • 44
  • 1
    Thanks but "No thanks", I think I'd become better at what I try if I could fix my own mess. I'll keep an eye on this library though. :) – mirx Apr 29 '14 at 17:53
1

Quick check- you are implementing the delegate methods:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {

    [self.tableView beginUpdates];
}

And:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {

    [self.tableView endUpdates];
}

Yes?

Dean Davids
  • 4,174
  • 2
  • 30
  • 44
1

In your NSFetchedResultControllerDelegate method I think you are getting tripped up by referring repeatedly to your properties in your View Controllers for the category and for the open status of that category. What I believe you should do is stick with the changed object.

Your detail item category is out of sync with the object. Start by changing the delegate method to this:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
    Feed *changedItem = (Feed *)object

    switch(type) {
        case NSFetchedResultsChangeInsert:
            NSLog(@"MasterViewController::didChangeObject **** Inserting something in %@**** ", changedItem.category.name);
            if ([changedItem.category.open boolValue]) {
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            break;

        case NSFetchedResultsChangeDelete:
            NSLog(@"MasterViewController::didChangeObject **** Deleting something in %@**** ", changedItem.category.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
        break;

        case NSFetchedResultsChangeUpdate:
            NSLog(@"MasterViewController::didChangeObject **** Changing something in %@**** ", changedItem.category.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            }
            break;

        case NSFetchedResultsChangeMove:
            NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);
            // I'm certain that here is where you are out of sync. If changedItem.category
            // is open, then all you need do is insert the moved item. If it is closed
            // you would need to remove all the rows in the category it came from.
            // 
            // The problem arises because category.open state does not necessarily reflect
            // the current state of the section. If the table or section is not yet refreshed
            // then the table state is unknown. We need to know the state of the table at the time
            // of the move in order to know what to do with the rows. On endUpdates the table will
            // call its delegate methods and they must match the results of what we have done here.
            // As a quick fix, given that I do not have all your models or project to work with,
            // We could set all categories open state to closed and collapse all sections prior to making
            // changes here. Better model would be to make the table respond directly any time a
            // category objects open state is changed. You could do that with KVO or NSNotifications perhaps.
            // In that scenario, you could always count on the table and the model to be in sync.

            if ([changedItem.category.open boolValue]) {
                // This becomes way complicated if you have not sync'd open and closed sections to match the category states prior to saving your context.
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            } else {
                // Again, if your table sections collapse state is in sync (and only one section
                // is ever open at one time) you need do nothing. Otherwise you'll need some gyrations
                // here to get everything to match what the delegate methods are ultimately going to return
            }

            break;
    }
}
Dean Davids
  • 4,174
  • 2
  • 30
  • 44
  • Just to clarify: the second bit of code is called from another UIView. Yet, I'd like to know how I can efficiently determine the Section to be reloaded from there, i.e. How to get an NSManagedObject's NSIndexPath in an NSUITableView? BTW, Should I perform a save each time I change one property, or wait until the method is over to save it all? – mirx Apr 30 '14 at 05:51
  • 1
    I hadn't considered that. I wouldn't recommend trying to manipulate the sections from another view. If I wanted to do that, I'd use a protocol but I have a better idea, will edit my answer. – Dean Davids Apr 30 '14 at 11:26
  • 1
    In the context of your question, it is not necessary to save to do what you are doing. You want to save when the user commits, changes views or dismisses app so they will not notice any delay or blocking of the UI. – Dean Davids Apr 30 '14 at 11:37
  • Thanks, I could also move the button to the Masterview Controller... But then why not deploy a complete additional dedicated controller to edit the Feeds, which might lead me back to the current situation... – mirx Apr 30 '14 at 13:44
  • 1
    Best case, you should have a KVO or NSNotification that will tell your tableViewController that a category is opened or closed. This way you would always know that the table is in sync with the model. With that taken care of, your current response is mostly correct. – Dean Davids Apr 30 '14 at 13:57
  • 1
    I may have mislead with my response to your comment about saving context. None of the NSFetchedResultsControllerDelegate methods will be called until you save the context. So, it is relevant. The key is to make sure your table sections collapsed state matches your category objects open state prior to committing that save. – Dean Davids Apr 30 '14 at 14:10
  • So I might as well wrap the open/closed flag change in a MasterViewController method that will actually open/close the Category as well as updating MOC? – mirx Apr 30 '14 at 15:08
  • 1
    That would work. I like to see it a bit more behind the scenes, but you can do it that way. The downside is that you have to consider anywhere that the category.open attribute changes and make sure you do not circumvent the process. – Dean Davids Apr 30 '14 at 16:07
  • Hello again and thanks. I think we can forget the "open" problematic, that's a sync problem but I don't understand why... Would you mind checking the UPDATE(3)? – mirx Apr 30 '14 at 17:01
  • 1
    You do not need to save for the NSFRC to fire. The only time a save is necessary is when you are making changes in a **different** context and want those changes propagated. Since this is all in one context no save is required. – Marcus S. Zarra Apr 30 '14 at 20:26
  • Hello Marcus, I just added a 4th update which describes what happen without the save. – mirx May 01 '14 at 05:34
0

The bug has been gone after I edited the didChangeObject method the following way (I have to take the open flag into account):

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
Category *category = ((Feed *) anObject).category;
switch(type) {
    case NSFetchedResultsChangeInsert:
        NSLog(@"MasterViewController::didChangeObject **** Inserting something in %@**** ", category.name);
        if ([category.open boolValue]) {
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

    case NSFetchedResultsChangeDelete:
        NSLog(@"MasterViewController::didChangeObject **** Deleting something in %@**** ", _detailViewController.detailCategory.name);

        if ([_detailViewController.detailCategory.open boolValue]) {
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

    case NSFetchedResultsChangeUpdate:
        NSLog(@"MasterViewController::didChangeObject **** Changing something in %@**** ", category.name);

        if ([category.open boolValue]) {
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
        }
        break;

    case NSFetchedResultsChangeMove:
        NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, category.name);

        if ([_detailViewController.detailCategory.open boolValue]) {
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        if ([category.open boolValue]) {
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;
}
}
mirx
  • 148
  • 11