Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom cell on empty table with NSFetchedResultsController

I'm trying to place a custom empty cell when my UITableView is empty.

I use the following code:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{

    int count = [self.fetchedResultsController fetchedObjects].count;
    if (count == 0) {
        return 1;
    }
    return count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell * cell = nil;
    int count = [self.fetchedResultsController fetchedObjects].count;
    if (count == 0) {
        // return empty cell
        cell = [self getEmptyCellOfTableView:tableView cellForRowAtIndexPath:indexPath];
    } else {
        cell = [self getMyQuestionCellOfTableView:tableView cellForRowAtIndexPath:indexPath];
    }
    return cell;
}

FRC didChangeObject implementation:

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{       
    if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
    {
        switch(type)
        {
            case NSFetchedResultsChangeInsert:
                [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                break;

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

            case NSFetchedResultsChangeUpdate:
                [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                break;

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

This works perfectly, the problam is when the fetchedObject.count > 0 than 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 (1)
must be equal to the number of rows contained in that section before the update (1),
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)

I understand that this happens because I inserted 1 new row and the count stays at 1 instead of becoming 2.

How can i fix it to fit my behavior?

like image 991
Danpe Avatar asked Dec 11 '22 13:12

Danpe


2 Answers

After a table view update, the value returned by numberOfRowsInSection must exactly match the previous number of rows plus the number of inserted rows minus the number of deleted rows.

In your case, when the first object is inserted, insertRowsAtIndexPaths is called in the FRC delegate method for the new object, but instead of displaying an additional row, the "empty" cell is replaced with a different cell, so the number of rows is still one.

It should work if you modify the FRC delegate method controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: as follows:

case NSFetchedResultsChangeInsert:
    if ([[self.fetchedResultsController fetchedObjects] count] == 1) {
        // First object inserted, "empty cell" is replaced by "object cell"
        [tableView reloadRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
    } else {
        [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
    }
    break;

case NSFetchedResultsChangeDelete:
    if ([[self.fetchedResultsController fetchedObjects] count] == 0) {
        // Last object removed, "object cell" is replaced by "empty cell"
        [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
    } else {
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
    }
    break;
like image 63
Martin R Avatar answered Jan 26 '23 18:01

Martin R


PSA: If you haven't checked out Yang Meyer's solution yet for displaying a cell on a empty table, I suggest you do. It's clever and a much simpler way of handling an empty table or empty NSFetchedResultsController.

Yang Meyer's Best Practice for Handling Empty Table Views

The way that I used it was I would in my viewDidLoad, I would initially assume that the results were empty. Only when my fetchedResultsController had objects, do I change the data source back.

FJFetchedDataController.h

@interface FJFetchedDataTableController : UITableViewController <NSFetchedResultsControllerDelegate, UITableViewDelegate>

@end

FJFetchedDataController.m

@implementation FJFetchedDataTableController {
@private
    FJEmptyDataSource *_emptyDataSource;
};

- (void)viewDidLoad {
    [super viewDidLoad];
    // create the empty data source
    _emptyDataSource = [[FJEmptyDataSource alloc] init];
    // point the tableView to the empty data source
    self.tableView.dataSource = _emptyDataSource;

    self.fetchedResultsController.delegate = self;

    // TODO: configure your fetchedResultsController here

    NSError *error;
    if (![self.fetchedResultsController performFetch:&error]) {
        // TODO: error handling
    }

    if ([self.fetchedResultsController.fetchedObjects count]) {
        // set the table source back to itself (or whatever you want as the data source)
        // since we know we have results
        self.tableView.dataSource = self;
        return;
    }
}

Simple, yet really effective.

like image 32
mikeho Avatar answered Jan 26 '23 18:01

mikeho